@mentra/engine 3.2.1-dev.288 → 3.2.1-dev.289
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/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/services/AcsMeetingService.d.ts +2 -9
- package/build/services/AcsMeetingService.d.ts.map +1 -1
- package/build/services/AcsMeetingService.js +2 -17
- package/build/services/AcsMeetingService.js.map +1 -1
- package/build/services/ManagedWebRtcRelay.d.ts +16 -1
- package/build/services/ManagedWebRtcRelay.d.ts.map +1 -1
- package/build/services/ManagedWebRtcRelay.js +59 -3
- package/build/services/ManagedWebRtcRelay.js.map +1 -1
- package/build/services/micPolicy.d.ts +1 -1
- package/build/services/micPolicy.d.ts.map +1 -1
- package/build/services/micPolicy.js +4 -1
- package/build/services/micPolicy.js.map +1 -1
- package/build/utils/pcmToBase64.d.ts +3 -0
- package/build/utils/pcmToBase64.d.ts.map +1 -0
- package/build/utils/pcmToBase64.js +11 -0
- package/build/utils/pcmToBase64.js.map +1 -0
- package/package.json +8 -8
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/services/AcsMeetingService.ts +2 -18
- package/src/services/ManagedWebRtcRelay.ts +63 -3
- package/src/services/micPolicy.ts +5 -2
- package/src/utils/pcmToBase64.ts +10 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import BluetoothSdk from "@mentra/bluetooth-sdk/internal"
|
|
2
2
|
import type {StreamStartRequest, StreamStatusEvent} from "@mentra/bluetooth-sdk/internal"
|
|
3
3
|
import {acquireGlassesHotspot} from "./GlassesHotspotLease"
|
|
4
|
+
import {pcmToBase64} from "../utils/pcmToBase64"
|
|
4
5
|
|
|
5
6
|
export interface RelayOptions {
|
|
6
7
|
streamId: string
|
|
@@ -24,9 +25,11 @@ interface NativeRelay {
|
|
|
24
25
|
password: string
|
|
25
26
|
gatewayAddress?: string
|
|
26
27
|
captureAudio: boolean
|
|
28
|
+
audioTransport: "whip" | "ble-lc3" | "none"
|
|
27
29
|
bitrate: number
|
|
28
30
|
}): Promise<string>
|
|
29
31
|
stop(attemptId: string): Promise<void>
|
|
32
|
+
pushOutgoingPcm?(attemptId: string, base64: string, sampleRate: number, channels: number): boolean
|
|
30
33
|
addListener(event: "onRelayState", listener: (event: NativeRelayEvent) => void): {remove(): void}
|
|
31
34
|
}
|
|
32
35
|
|
|
@@ -40,6 +43,11 @@ export interface RelayDependencies {
|
|
|
40
43
|
sleep(ms: number): Promise<void>
|
|
41
44
|
now(): number
|
|
42
45
|
acquire(): () => void
|
|
46
|
+
/** Present only on hosts that can pin and decode the glasses' BLE microphone, as ACS does. */
|
|
47
|
+
microphone?: {
|
|
48
|
+
acquire(): () => void
|
|
49
|
+
subscribe(listener: (event: {pcm?: ArrayBuffer; sampleRate?: number; source?: string}) => void): {remove(): void}
|
|
50
|
+
}
|
|
43
51
|
}
|
|
44
52
|
|
|
45
53
|
export interface ManagedRelay {
|
|
@@ -50,7 +58,7 @@ export interface ManagedRelay {
|
|
|
50
58
|
handleGlassesStatus(event: StreamStatusEvent): void
|
|
51
59
|
}
|
|
52
60
|
|
|
53
|
-
/** One attempt at a time;
|
|
61
|
+
/** One attempt at a time; video stays native. All retries unwind both peers, mic and hotspot. */
|
|
54
62
|
export class ManagedWebRtcRelay implements ManagedRelay {
|
|
55
63
|
private cancelled = false
|
|
56
64
|
private started = false
|
|
@@ -67,6 +75,8 @@ export class ManagedWebRtcRelay implements ManagedRelay {
|
|
|
67
75
|
private retries = 0
|
|
68
76
|
private readyAt: number | null = null
|
|
69
77
|
private stopping: Promise<void> | null = null
|
|
78
|
+
private releaseMic: (() => void) | null = null
|
|
79
|
+
private micListener: {remove(): void} | null = null
|
|
70
80
|
|
|
71
81
|
constructor(
|
|
72
82
|
private readonly options: RelayOptions,
|
|
@@ -114,6 +124,10 @@ export class ManagedWebRtcRelay implements ManagedRelay {
|
|
|
114
124
|
this.attemptId = `${this.options.streamId}-relay-${++this.attempt}`
|
|
115
125
|
this.attemptError = null
|
|
116
126
|
this.checkpoint()
|
|
127
|
+
const captureAudio = this.options.captureAudio !== false
|
|
128
|
+
const lc3 = captureAudio && !!this.deps.microphone && typeof this.deps.native.pushOutgoingPcm === "function"
|
|
129
|
+
// Claim through the same policy/ownership layer as ACS before turning off WHIP audio.
|
|
130
|
+
if (lc3) this.releaseMic = this.deps.microphone!.acquire()
|
|
117
131
|
this.hotspotTouched = true // A timed-out BLE command may still have enabled the AP.
|
|
118
132
|
const hotspot = await this.deps.hotspot(true)
|
|
119
133
|
this.checkpoint()
|
|
@@ -128,10 +142,35 @@ export class ManagedWebRtcRelay implements ManagedRelay {
|
|
|
128
142
|
ssid: hotspot.ssid,
|
|
129
143
|
password: hotspot.password,
|
|
130
144
|
gatewayAddress: hotspot.localIp,
|
|
131
|
-
captureAudio
|
|
145
|
+
captureAudio,
|
|
146
|
+
audioTransport: lc3 ? "ble-lc3" : captureAudio ? "whip" : "none",
|
|
132
147
|
bitrate: this.options.video?.bitrate ?? 2_000_000,
|
|
133
148
|
})
|
|
134
149
|
this.checkpoint()
|
|
150
|
+
if (lc3) {
|
|
151
|
+
const id = this.attemptId
|
|
152
|
+
let received = false
|
|
153
|
+
this.micListener = this.deps.microphone!.subscribe((event) => {
|
|
154
|
+
if (this.cancelled || this.attemptId !== id || this.attemptError) return
|
|
155
|
+
if (event.source !== "glasses") {
|
|
156
|
+
this.failed(new Error("Managed stream lost the glasses microphone"))
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
if (!event.pcm?.byteLength) return
|
|
160
|
+
try {
|
|
161
|
+
const accepted = this.deps.native.pushOutgoingPcm!(id, pcmToBase64(event.pcm), event.sampleRate ?? 16000, 1)
|
|
162
|
+
if (accepted && !received) {
|
|
163
|
+
received = true
|
|
164
|
+
console.log("[ManagedRelay] First BLE LC3 audio frame", {
|
|
165
|
+
attemptId: id,
|
|
166
|
+
sampleRate: event.sampleRate ?? 16000,
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
} catch (error) {
|
|
170
|
+
this.failed(asError(error))
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
}
|
|
135
174
|
this.glassesTouched = true
|
|
136
175
|
const result = await this.deps.startGlasses({
|
|
137
176
|
type: "start_stream",
|
|
@@ -139,7 +178,7 @@ export class ManagedWebRtcRelay implements ManagedRelay {
|
|
|
139
178
|
streamUrl: url,
|
|
140
179
|
ice: {stun: ""},
|
|
141
180
|
sound: this.options.sound ?? true,
|
|
142
|
-
captureAudio:
|
|
181
|
+
captureAudio: captureAudio && !lc3,
|
|
143
182
|
...(this.options.video !== undefined ? {video: this.options.video} : {}),
|
|
144
183
|
...(this.options.audio !== undefined ? {audio: this.options.audio} : {}),
|
|
145
184
|
})
|
|
@@ -223,6 +262,12 @@ export class ManagedWebRtcRelay implements ManagedRelay {
|
|
|
223
262
|
// Invalidate callbacks before intentionally stopping the glasses or dropping the network.
|
|
224
263
|
const id = this.attemptId
|
|
225
264
|
this.attemptId = null
|
|
265
|
+
await step(async () => {
|
|
266
|
+
this.micListener?.remove()
|
|
267
|
+
this.micListener = null
|
|
268
|
+
this.releaseMic?.()
|
|
269
|
+
this.releaseMic = null
|
|
270
|
+
})
|
|
226
271
|
if (this.glassesTouched)
|
|
227
272
|
await step(async () => {
|
|
228
273
|
if (this.deps.connected()) await this.deps.stopGlasses()
|
|
@@ -264,6 +309,8 @@ export function createManagedWebRtcRelay(
|
|
|
264
309
|
const {requireNativeModule} = require("expo-modules-core") as typeof import("expo-modules-core")
|
|
265
310
|
const {BgTimer} = require("../utils/timers") as typeof import("../utils/timers")
|
|
266
311
|
const native = requireNativeModule<NativeRelay>("MentraGlassesMediaRelay")
|
|
312
|
+
const {Platform} = require("react-native") as typeof import("react-native")
|
|
313
|
+
const {default: micSessionManager} = require("./MicSessionManager") as typeof import("./MicSessionManager")
|
|
267
314
|
return new ManagedWebRtcRelay(options, onStatus, onFailure, {
|
|
268
315
|
native,
|
|
269
316
|
hotspot: (enabled) => BluetoothSdk.setHotspotState(enabled),
|
|
@@ -274,5 +321,18 @@ export function createManagedWebRtcRelay(
|
|
|
274
321
|
now: Date.now,
|
|
275
322
|
sleep: (ms) => new Promise((resolve) => BgTimer.setTimeout(resolve, ms)),
|
|
276
323
|
acquire: acquireGlassesHotspot,
|
|
324
|
+
...(Platform.OS === "android" && typeof BluetoothSdk.setMicSourcePin === "function"
|
|
325
|
+
? {
|
|
326
|
+
microphone: {
|
|
327
|
+
acquire: () =>
|
|
328
|
+
micSessionManager.acquire({
|
|
329
|
+
owner: `engine:managed-relay:${options.streamId}`,
|
|
330
|
+
source: "glasses",
|
|
331
|
+
useCase: "livestream",
|
|
332
|
+
}).release,
|
|
333
|
+
subscribe: (listener) => BluetoothSdk.addListener("mic_pcm", listener),
|
|
334
|
+
},
|
|
335
|
+
}
|
|
336
|
+
: {}),
|
|
277
337
|
})
|
|
278
338
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
/** What an application is doing with the microphone. */
|
|
14
|
-
export type MicUseCase = "voice_call" | "transcription" | "voice_assistant" | "diagnostic"
|
|
14
|
+
export type MicUseCase = "voice_call" | "transcription" | "voice_assistant" | "diagnostic" | "livestream"
|
|
15
15
|
|
|
16
16
|
/** Which microphone the audio comes from. */
|
|
17
17
|
export type MicSource = "glasses" | "phone"
|
|
@@ -117,6 +117,7 @@ export const MIC_USE_CASE_PROFILES: Record<MicUseCase, MicTuningProfile> = {
|
|
|
117
117
|
transcription: {},
|
|
118
118
|
voice_assistant: {},
|
|
119
119
|
diagnostic: {},
|
|
120
|
+
livestream: {}, // Preserve the configured capture gain; there is no call return audio.
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
/**
|
|
@@ -135,6 +136,7 @@ export const MIC_USE_CASE_LOUDNESS_GATE: Record<MicUseCase, boolean> = {
|
|
|
135
136
|
transcription: false,
|
|
136
137
|
voice_assistant: false,
|
|
137
138
|
diagnostic: false,
|
|
139
|
+
livestream: false,
|
|
138
140
|
}
|
|
139
141
|
|
|
140
142
|
/**
|
|
@@ -151,13 +153,14 @@ export const MIC_USE_CASES: readonly MicUseCase[] = [
|
|
|
151
153
|
"transcription",
|
|
152
154
|
"voice_assistant",
|
|
153
155
|
"diagnostic",
|
|
156
|
+
"livestream",
|
|
154
157
|
]
|
|
155
158
|
|
|
156
159
|
/** Owners of engine-internal sessions. Miniapps cannot claim these use cases. */
|
|
157
160
|
export const ENGINE_OWNER_PREFIX = "engine:"
|
|
158
161
|
|
|
159
162
|
/** Use cases only engine features may acquire. */
|
|
160
|
-
export const ENGINE_ONLY_USE_CASES: readonly MicUseCase[] = ["diagnostic"]
|
|
163
|
+
export const ENGINE_ONLY_USE_CASES: readonly MicUseCase[] = ["diagnostic", "livestream"]
|
|
161
164
|
|
|
162
165
|
/**
|
|
163
166
|
* Resolve every live session into one hardware state.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Encode decoded LC3 PCM for native sinks. Hermes has btoa, but no Node Buffer. */
|
|
2
|
+
export function pcmToBase64(pcm: ArrayBuffer): string {
|
|
3
|
+
const bytes = new Uint8Array(pcm)
|
|
4
|
+
let binary = ""
|
|
5
|
+
const step = 0x8000
|
|
6
|
+
for (let i = 0; i < bytes.length; i += step) {
|
|
7
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + step))
|
|
8
|
+
}
|
|
9
|
+
return btoa(binary)
|
|
10
|
+
}
|