@mentra/engine 3.2.1-dev.288 → 3.2.1-dev.291

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.
@@ -20,9 +20,11 @@ interface NativeRelay {
20
20
  password: string;
21
21
  gatewayAddress?: string;
22
22
  captureAudio: boolean;
23
+ audioTransport: "whip" | "ble-lc3" | "none";
23
24
  bitrate: number;
24
25
  }): Promise<string>;
25
26
  stop(attemptId: string): Promise<void>;
27
+ pushOutgoingPcm?(attemptId: string, base64: string, sampleRate: number, channels: number): boolean;
26
28
  addListener(event: "onRelayState", listener: (event: NativeRelayEvent) => void): {
27
29
  remove(): void;
28
30
  };
@@ -42,6 +44,17 @@ export interface RelayDependencies {
42
44
  sleep(ms: number): Promise<void>;
43
45
  now(): number;
44
46
  acquire(): () => void;
47
+ /** Present only on hosts that can pin and decode the glasses' BLE microphone, as ACS does. */
48
+ microphone?: {
49
+ acquire(): () => void;
50
+ subscribe(listener: (event: {
51
+ pcm?: ArrayBuffer;
52
+ sampleRate?: number;
53
+ source?: string;
54
+ }) => void): {
55
+ remove(): void;
56
+ };
57
+ };
45
58
  }
46
59
  export interface ManagedRelay {
47
60
  start(): Promise<StreamStatusEvent | undefined>;
@@ -50,7 +63,7 @@ export interface ManagedRelay {
50
63
  owns(streamId: string): boolean;
51
64
  handleGlassesStatus(event: StreamStatusEvent): void;
52
65
  }
53
- /** One attempt at a time; media stays native. All retries unwind both peers and the hotspot. */
66
+ /** One attempt at a time; video stays native. All retries unwind both peers, mic and hotspot. */
54
67
  export declare class ManagedWebRtcRelay implements ManagedRelay {
55
68
  private readonly options;
56
69
  private readonly onStatus;
@@ -71,6 +84,8 @@ export declare class ManagedWebRtcRelay implements ManagedRelay {
71
84
  private retries;
72
85
  private readyAt;
73
86
  private stopping;
87
+ private releaseMic;
88
+ private micListener;
74
89
  constructor(options: RelayOptions, onStatus: (status: string, reason: string) => void, onFailure: (error: Error) => void, deps: RelayDependencies);
75
90
  start(): Promise<StreamStatusEvent | undefined>;
76
91
  cancel(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"ManagedWebRtcRelay.d.ts","sourceRoot":"","sources":["../../src/services/ManagedWebRtcRelay.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ManagedWebRtcRelay.d.ts","sourceRoot":"","sources":["../../src/services/ManagedWebRtcRelay.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  import BluetoothSdk from "@mentra/bluetooth-sdk/internal";
2
2
  import { acquireGlassesHotspot } from "./GlassesHotspotLease";
3
- /** One attempt at a time; media stays native. All retries unwind both peers and the hotspot. */
3
+ import { pcmToBase64 } from "../utils/pcmToBase64";
4
+ /** One attempt at a time; video stays native. All retries unwind both peers, mic and hotspot. */
4
5
  export class ManagedWebRtcRelay {
5
6
  options;
6
7
  onStatus;
@@ -21,6 +22,8 @@ export class ManagedWebRtcRelay {
21
22
  retries = 0;
22
23
  readyAt = null;
23
24
  stopping = null;
25
+ releaseMic = null;
26
+ micListener = null;
24
27
  constructor(options, onStatus, onFailure, deps) {
25
28
  this.options = options;
26
29
  this.onStatus = onStatus;
@@ -68,6 +71,11 @@ export class ManagedWebRtcRelay {
68
71
  this.attemptId = `${this.options.streamId}-relay-${++this.attempt}`;
69
72
  this.attemptError = null;
70
73
  this.checkpoint();
74
+ const captureAudio = this.options.captureAudio !== false;
75
+ const lc3 = captureAudio && !!this.deps.microphone && typeof this.deps.native.pushOutgoingPcm === "function";
76
+ // Claim through the same policy/ownership layer as ACS before turning off WHIP audio.
77
+ if (lc3)
78
+ this.releaseMic = this.deps.microphone.acquire();
71
79
  this.hotspotTouched = true; // A timed-out BLE command may still have enabled the AP.
72
80
  const hotspot = await this.deps.hotspot(true);
73
81
  this.checkpoint();
@@ -82,10 +90,38 @@ export class ManagedWebRtcRelay {
82
90
  ssid: hotspot.ssid,
83
91
  password: hotspot.password,
84
92
  gatewayAddress: hotspot.localIp,
85
- captureAudio: this.options.captureAudio !== false,
93
+ captureAudio,
94
+ audioTransport: lc3 ? "ble-lc3" : captureAudio ? "whip" : "none",
86
95
  bitrate: this.options.video?.bitrate ?? 2_000_000,
87
96
  });
88
97
  this.checkpoint();
98
+ if (lc3) {
99
+ const id = this.attemptId;
100
+ let received = false;
101
+ this.micListener = this.deps.microphone.subscribe((event) => {
102
+ if (this.cancelled || this.attemptId !== id || this.attemptError)
103
+ return;
104
+ if (event.source !== "glasses") {
105
+ this.failed(new Error("Managed stream lost the glasses microphone"));
106
+ return;
107
+ }
108
+ if (!event.pcm?.byteLength)
109
+ return;
110
+ try {
111
+ const accepted = this.deps.native.pushOutgoingPcm(id, pcmToBase64(event.pcm), event.sampleRate ?? 16000, 1);
112
+ if (accepted && !received) {
113
+ received = true;
114
+ console.log("[ManagedRelay] First BLE LC3 audio frame", {
115
+ attemptId: id,
116
+ sampleRate: event.sampleRate ?? 16000,
117
+ });
118
+ }
119
+ }
120
+ catch (error) {
121
+ this.failed(asError(error));
122
+ }
123
+ });
124
+ }
89
125
  this.glassesTouched = true;
90
126
  const result = await this.deps.startGlasses({
91
127
  type: "start_stream",
@@ -93,7 +129,7 @@ export class ManagedWebRtcRelay {
93
129
  streamUrl: url,
94
130
  ice: { stun: "" },
95
131
  sound: this.options.sound ?? true,
96
- captureAudio: this.options.captureAudio !== false,
132
+ captureAudio: captureAudio && !lc3,
97
133
  ...(this.options.video !== undefined ? { video: this.options.video } : {}),
98
134
  ...(this.options.audio !== undefined ? { audio: this.options.audio } : {}),
99
135
  });
@@ -186,6 +222,12 @@ export class ManagedWebRtcRelay {
186
222
  // Invalidate callbacks before intentionally stopping the glasses or dropping the network.
187
223
  const id = this.attemptId;
188
224
  this.attemptId = null;
225
+ await step(async () => {
226
+ this.micListener?.remove();
227
+ this.micListener = null;
228
+ this.releaseMic?.();
229
+ this.releaseMic = null;
230
+ });
189
231
  if (this.glassesTouched)
190
232
  await step(async () => {
191
233
  if (this.deps.connected())
@@ -224,6 +266,8 @@ export function createManagedWebRtcRelay(options, onStatus, onFailure, connected
224
266
  const { requireNativeModule } = require("expo-modules-core");
225
267
  const { BgTimer } = require("../utils/timers");
226
268
  const native = requireNativeModule("MentraGlassesMediaRelay");
269
+ const { Platform } = require("react-native");
270
+ const { default: micSessionManager } = require("./MicSessionManager");
227
271
  return new ManagedWebRtcRelay(options, onStatus, onFailure, {
228
272
  native,
229
273
  hotspot: (enabled) => BluetoothSdk.setHotspotState(enabled),
@@ -234,6 +278,18 @@ export function createManagedWebRtcRelay(options, onStatus, onFailure, connected
234
278
  now: Date.now,
235
279
  sleep: (ms) => new Promise((resolve) => BgTimer.setTimeout(resolve, ms)),
236
280
  acquire: acquireGlassesHotspot,
281
+ ...(Platform.OS === "android" && typeof BluetoothSdk.setMicSourcePin === "function"
282
+ ? {
283
+ microphone: {
284
+ acquire: () => micSessionManager.acquire({
285
+ owner: `engine:managed-relay:${options.streamId}`,
286
+ source: "glasses",
287
+ useCase: "livestream",
288
+ }).release,
289
+ subscribe: (listener) => BluetoothSdk.addListener("mic_pcm", listener),
290
+ },
291
+ }
292
+ : {}),
237
293
  });
238
294
  }
239
295
  //# sourceMappingURL=ManagedWebRtcRelay.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ManagedWebRtcRelay.js","sourceRoot":"","sources":["../../src/services/ManagedWebRtcRelay.ts"],"names":[],"mappings":"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","sourcesContent":["import BluetoothSdk from \"@mentra/bluetooth-sdk/internal\"\nimport type {StreamStartRequest, StreamStatusEvent} from \"@mentra/bluetooth-sdk/internal\"\nimport {acquireGlassesHotspot} from \"./GlassesHotspotLease\"\n\nexport interface RelayOptions {\n streamId: string\n ingestUrl: string\n video?: StreamStartRequest[\"video\"]\n audio?: StreamStartRequest[\"audio\"]\n captureAudio?: boolean\n sound?: boolean\n}\n\ninterface NativeRelayEvent {\n attemptId: string\n state: string\n reason: string\n}\ninterface NativeRelay {\n prepare(options: {\n attemptId: string\n ingestUrl: string\n ssid: string\n password: string\n gatewayAddress?: string\n captureAudio: boolean\n bitrate: number\n }): Promise<string>\n stop(attemptId: string): Promise<void>\n addListener(event: \"onRelayState\", listener: (event: NativeRelayEvent) => void): {remove(): void}\n}\n\nexport interface RelayDependencies {\n native: NativeRelay\n hotspot(enabled: boolean): Promise<{state: string; ssid?: string; password?: string; localIp?: string}>\n startGlasses(request: StreamStartRequest): Promise<StreamStatusEvent | undefined>\n stopGlasses(): Promise<unknown>\n deferredStop(): void\n connected(): boolean\n sleep(ms: number): Promise<void>\n now(): number\n acquire(): () => void\n}\n\nexport interface ManagedRelay {\n start(): Promise<StreamStatusEvent | undefined>\n cancel(): void\n stop(): Promise<void>\n owns(streamId: string): boolean\n handleGlassesStatus(event: StreamStatusEvent): void\n}\n\n/** One attempt at a time; media stays native. All retries unwind both peers and the hotspot. */\nexport class ManagedWebRtcRelay implements ManagedRelay {\n private cancelled = false\n private started = false\n private attempt = 0\n private attemptId: string | null = null\n private attemptError: Error | null = null\n private hotspotTouched = false\n private nativeTouched = false\n private glassesTouched = false\n private release: (() => void) | null = null\n private listener: {remove(): void} | null = null\n private operation: Promise<unknown> = Promise.resolve()\n private restarting = false\n private retries = 0\n private readyAt: number | null = null\n private stopping: Promise<void> | null = null\n\n constructor(\n private readonly options: RelayOptions,\n private readonly onStatus: (status: string, reason: string) => void,\n private readonly onFailure: (error: Error) => void,\n private readonly deps: RelayDependencies,\n ) {}\n\n start(): Promise<StreamStatusEvent | undefined> {\n if (this.started) return Promise.reject(new Error(\"Relay already started\"))\n this.started = true\n this.release = this.deps.acquire()\n this.listener = this.deps.native.addListener(\"onRelayState\", (event) => {\n if (event.attemptId !== this.attemptId || this.cancelled) return\n if (event.state === \"failed\") this.failed(new Error(event.reason))\n else this.onStatus(event.state, event.reason)\n })\n const start = this.startAttempt()\n this.operation = start\n return start\n }\n\n cancel(): void {\n this.cancelled = true\n }\n\n owns(streamId: string): boolean {\n // Consume late status from all attempts without allowing it to affect the current attempt.\n return streamId.startsWith(`${this.options.streamId}-relay-`)\n }\n\n handleGlassesStatus(event: StreamStatusEvent): void {\n if (event.streamId !== this.attemptId || this.cancelled) return\n if (event.status === \"stopped\" || event.terminal) {\n this.failed(new Error(\"Glasses publisher stopped\"))\n }\n }\n\n private checkpoint(): void {\n if (this.cancelled) throw new Error(\"Relay cancelled\")\n if (this.attemptError) throw this.attemptError\n }\n\n private async startAttempt(): Promise<StreamStatusEvent | undefined> {\n this.attemptId = `${this.options.streamId}-relay-${++this.attempt}`\n this.attemptError = null\n this.checkpoint()\n this.hotspotTouched = true // A timed-out BLE command may still have enabled the AP.\n const hotspot = await this.deps.hotspot(true)\n this.checkpoint()\n if (hotspot.state !== \"enabled\" || !hotspot.ssid || !hotspot.password)\n throw new Error(\"Glasses hotspot did not start\")\n await this.deps.sleep(3_000) // Same beacon/DHCP startup allowance as ACS.\n this.checkpoint()\n this.nativeTouched = true\n const url = await this.deps.native.prepare({\n attemptId: this.attemptId,\n ingestUrl: this.options.ingestUrl,\n ssid: hotspot.ssid,\n password: hotspot.password,\n gatewayAddress: hotspot.localIp,\n captureAudio: this.options.captureAudio !== false,\n bitrate: this.options.video?.bitrate ?? 2_000_000,\n })\n this.checkpoint()\n this.glassesTouched = true\n const result = await this.deps.startGlasses({\n type: \"start_stream\",\n streamId: this.attemptId,\n streamUrl: url,\n ice: {stun: \"\"},\n sound: this.options.sound ?? true,\n captureAudio: this.options.captureAudio !== false,\n ...(this.options.video !== undefined ? {video: this.options.video} : {}),\n ...(this.options.audio !== undefined ? {audio: this.options.audio} : {}),\n })\n this.checkpoint()\n this.readyAt = this.deps.now()\n return result\n }\n\n private failed(error: Error): void {\n if (this.cancelled || this.attemptError) return\n this.attemptError = error\n // Bound consecutive trouble, not the lifetime number of recoveries in a long stream.\n if (this.readyAt !== null && this.deps.now() - this.readyAt >= 60_000) this.retries = 0\n this.readyAt = null\n if (this.restarting) return\n this.restarting = true\n // Await startup before cleanup: late native/BLE success still belongs to this attempt.\n this.operation = this.operation\n .catch(() => undefined)\n .then(async () => {\n while (!this.cancelled) {\n try {\n await this.cleanupAttempt()\n if (this.cancelled) return\n if (this.retries >= 3) throw error\n this.onStatus(\"reconnecting\", error.message)\n await this.deps.sleep(1_000 * 2 ** this.retries++)\n if (this.cancelled) return\n await this.startAttempt()\n this.onStatus(\"reconnected\", \"Glasses relay restarted\")\n return\n } catch (failure) {\n // Cleanup failure retains ownership; never create another stream on uncertain state.\n if (this.hotspotTouched || this.nativeTouched || this.glassesTouched) {\n try {\n await this.cleanupAttempt()\n } catch (cleanupError) {\n if (!this.cancelled) this.onFailure(asError(cleanupError))\n return\n }\n }\n if (this.retries >= 3) {\n if (!this.cancelled) this.onFailure(asError(failure))\n return\n }\n }\n }\n })\n .finally(() => {\n this.restarting = false\n })\n }\n\n stop(): Promise<void> {\n this.cancel()\n if (this.stopping) return this.stopping\n this.listener?.remove()\n this.listener = null\n this.stopping = this.operation\n .catch(() => undefined)\n .then(async () => {\n await this.cleanupAttempt()\n this.release?.()\n this.release = null\n })\n .finally(() => {\n this.stopping = null\n })\n return this.stopping\n }\n\n private async cleanupAttempt(): Promise<void> {\n const failures: unknown[] = []\n const step = async (action: () => Promise<void>) => {\n try {\n await action()\n } catch (error) {\n failures.push(error)\n }\n }\n // Invalidate callbacks before intentionally stopping the glasses or dropping the network.\n const id = this.attemptId\n this.attemptId = null\n if (this.glassesTouched)\n await step(async () => {\n if (this.deps.connected()) await this.deps.stopGlasses()\n else this.deps.deferredStop()\n this.glassesTouched = false\n })\n if (this.nativeTouched && id)\n await step(async () => {\n await this.deps.native.stop(id)\n this.nativeTouched = false\n })\n if (this.hotspotTouched)\n await step(async () => {\n if (this.deps.connected()) {\n const result = await this.deps.hotspot(false)\n if (result.state !== \"disabled\") throw new Error(\"Glasses hotspot shutdown was not confirmed\")\n } else this.deps.deferredStop()\n this.hotspotTouched = false\n })\n if (failures.length) {\n this.attemptId = id // Retain the native attempt token for a subsequent cleanup retry.\n throw new Error(`Relay cleanup failed: ${failures.map((error) => asError(error).message).join(\"; \")}`)\n }\n }\n}\n\nfunction asError(error: unknown): Error {\n return error instanceof Error ? error : new Error(String(error))\n}\n\nexport function createManagedWebRtcRelay(\n options: RelayOptions,\n onStatus: (status: string, reason: string) => void,\n onFailure: (error: Error) => void,\n connected: () => boolean,\n deferredStop: () => void,\n): ManagedRelay {\n // Load only for managed WHIP. Hosts using direct SRT/RTMP need no relay module.\n const {requireNativeModule} = require(\"expo-modules-core\") as typeof import(\"expo-modules-core\")\n const {BgTimer} = require(\"../utils/timers\") as typeof import(\"../utils/timers\")\n const native = requireNativeModule<NativeRelay>(\"MentraGlassesMediaRelay\")\n return new ManagedWebRtcRelay(options, onStatus, onFailure, {\n native,\n hotspot: (enabled) => BluetoothSdk.setHotspotState(enabled),\n startGlasses: (request) => BluetoothSdk.startStream(request),\n stopGlasses: () => BluetoothSdk.stopStream(),\n connected,\n deferredStop,\n now: Date.now,\n sleep: (ms) => new Promise((resolve) => BgTimer.setTimeout(resolve, ms)),\n acquire: acquireGlassesHotspot,\n })\n}\n"]}
1
+ {"version":3,"file":"ManagedWebRtcRelay.js","sourceRoot":"","sources":["../../src/services/ManagedWebRtcRelay.ts"],"names":[],"mappings":"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","sourcesContent":["import BluetoothSdk from \"@mentra/bluetooth-sdk/internal\"\nimport type {StreamStartRequest, StreamStatusEvent} from \"@mentra/bluetooth-sdk/internal\"\nimport {acquireGlassesHotspot} from \"./GlassesHotspotLease\"\nimport {pcmToBase64} from \"../utils/pcmToBase64\"\n\nexport interface RelayOptions {\n streamId: string\n ingestUrl: string\n video?: StreamStartRequest[\"video\"]\n audio?: StreamStartRequest[\"audio\"]\n captureAudio?: boolean\n sound?: boolean\n}\n\ninterface NativeRelayEvent {\n attemptId: string\n state: string\n reason: string\n}\ninterface NativeRelay {\n prepare(options: {\n attemptId: string\n ingestUrl: string\n ssid: string\n password: string\n gatewayAddress?: string\n captureAudio: boolean\n audioTransport: \"whip\" | \"ble-lc3\" | \"none\"\n bitrate: number\n }): Promise<string>\n stop(attemptId: string): Promise<void>\n pushOutgoingPcm?(attemptId: string, base64: string, sampleRate: number, channels: number): boolean\n addListener(event: \"onRelayState\", listener: (event: NativeRelayEvent) => void): {remove(): void}\n}\n\nexport interface RelayDependencies {\n native: NativeRelay\n hotspot(enabled: boolean): Promise<{state: string; ssid?: string; password?: string; localIp?: string}>\n startGlasses(request: StreamStartRequest): Promise<StreamStatusEvent | undefined>\n stopGlasses(): Promise<unknown>\n deferredStop(): void\n connected(): boolean\n sleep(ms: number): Promise<void>\n now(): number\n acquire(): () => void\n /** Present only on hosts that can pin and decode the glasses' BLE microphone, as ACS does. */\n microphone?: {\n acquire(): () => void\n subscribe(listener: (event: {pcm?: ArrayBuffer; sampleRate?: number; source?: string}) => void): {remove(): void}\n }\n}\n\nexport interface ManagedRelay {\n start(): Promise<StreamStatusEvent | undefined>\n cancel(): void\n stop(): Promise<void>\n owns(streamId: string): boolean\n handleGlassesStatus(event: StreamStatusEvent): void\n}\n\n/** One attempt at a time; video stays native. All retries unwind both peers, mic and hotspot. */\nexport class ManagedWebRtcRelay implements ManagedRelay {\n private cancelled = false\n private started = false\n private attempt = 0\n private attemptId: string | null = null\n private attemptError: Error | null = null\n private hotspotTouched = false\n private nativeTouched = false\n private glassesTouched = false\n private release: (() => void) | null = null\n private listener: {remove(): void} | null = null\n private operation: Promise<unknown> = Promise.resolve()\n private restarting = false\n private retries = 0\n private readyAt: number | null = null\n private stopping: Promise<void> | null = null\n private releaseMic: (() => void) | null = null\n private micListener: {remove(): void} | null = null\n\n constructor(\n private readonly options: RelayOptions,\n private readonly onStatus: (status: string, reason: string) => void,\n private readonly onFailure: (error: Error) => void,\n private readonly deps: RelayDependencies,\n ) {}\n\n start(): Promise<StreamStatusEvent | undefined> {\n if (this.started) return Promise.reject(new Error(\"Relay already started\"))\n this.started = true\n this.release = this.deps.acquire()\n this.listener = this.deps.native.addListener(\"onRelayState\", (event) => {\n if (event.attemptId !== this.attemptId || this.cancelled) return\n if (event.state === \"failed\") this.failed(new Error(event.reason))\n else this.onStatus(event.state, event.reason)\n })\n const start = this.startAttempt()\n this.operation = start\n return start\n }\n\n cancel(): void {\n this.cancelled = true\n }\n\n owns(streamId: string): boolean {\n // Consume late status from all attempts without allowing it to affect the current attempt.\n return streamId.startsWith(`${this.options.streamId}-relay-`)\n }\n\n handleGlassesStatus(event: StreamStatusEvent): void {\n if (event.streamId !== this.attemptId || this.cancelled) return\n if (event.status === \"stopped\" || event.terminal) {\n this.failed(new Error(\"Glasses publisher stopped\"))\n }\n }\n\n private checkpoint(): void {\n if (this.cancelled) throw new Error(\"Relay cancelled\")\n if (this.attemptError) throw this.attemptError\n }\n\n private async startAttempt(): Promise<StreamStatusEvent | undefined> {\n this.attemptId = `${this.options.streamId}-relay-${++this.attempt}`\n this.attemptError = null\n this.checkpoint()\n const captureAudio = this.options.captureAudio !== false\n const lc3 = captureAudio && !!this.deps.microphone && typeof this.deps.native.pushOutgoingPcm === \"function\"\n // Claim through the same policy/ownership layer as ACS before turning off WHIP audio.\n if (lc3) this.releaseMic = this.deps.microphone!.acquire()\n this.hotspotTouched = true // A timed-out BLE command may still have enabled the AP.\n const hotspot = await this.deps.hotspot(true)\n this.checkpoint()\n if (hotspot.state !== \"enabled\" || !hotspot.ssid || !hotspot.password)\n throw new Error(\"Glasses hotspot did not start\")\n await this.deps.sleep(3_000) // Same beacon/DHCP startup allowance as ACS.\n this.checkpoint()\n this.nativeTouched = true\n const url = await this.deps.native.prepare({\n attemptId: this.attemptId,\n ingestUrl: this.options.ingestUrl,\n ssid: hotspot.ssid,\n password: hotspot.password,\n gatewayAddress: hotspot.localIp,\n captureAudio,\n audioTransport: lc3 ? \"ble-lc3\" : captureAudio ? \"whip\" : \"none\",\n bitrate: this.options.video?.bitrate ?? 2_000_000,\n })\n this.checkpoint()\n if (lc3) {\n const id = this.attemptId\n let received = false\n this.micListener = this.deps.microphone!.subscribe((event) => {\n if (this.cancelled || this.attemptId !== id || this.attemptError) return\n if (event.source !== \"glasses\") {\n this.failed(new Error(\"Managed stream lost the glasses microphone\"))\n return\n }\n if (!event.pcm?.byteLength) return\n try {\n const accepted = this.deps.native.pushOutgoingPcm!(id, pcmToBase64(event.pcm), event.sampleRate ?? 16000, 1)\n if (accepted && !received) {\n received = true\n console.log(\"[ManagedRelay] First BLE LC3 audio frame\", {\n attemptId: id,\n sampleRate: event.sampleRate ?? 16000,\n })\n }\n } catch (error) {\n this.failed(asError(error))\n }\n })\n }\n this.glassesTouched = true\n const result = await this.deps.startGlasses({\n type: \"start_stream\",\n streamId: this.attemptId,\n streamUrl: url,\n ice: {stun: \"\"},\n sound: this.options.sound ?? true,\n captureAudio: captureAudio && !lc3,\n ...(this.options.video !== undefined ? {video: this.options.video} : {}),\n ...(this.options.audio !== undefined ? {audio: this.options.audio} : {}),\n })\n this.checkpoint()\n this.readyAt = this.deps.now()\n return result\n }\n\n private failed(error: Error): void {\n if (this.cancelled || this.attemptError) return\n this.attemptError = error\n // Bound consecutive trouble, not the lifetime number of recoveries in a long stream.\n if (this.readyAt !== null && this.deps.now() - this.readyAt >= 60_000) this.retries = 0\n this.readyAt = null\n if (this.restarting) return\n this.restarting = true\n // Await startup before cleanup: late native/BLE success still belongs to this attempt.\n this.operation = this.operation\n .catch(() => undefined)\n .then(async () => {\n while (!this.cancelled) {\n try {\n await this.cleanupAttempt()\n if (this.cancelled) return\n if (this.retries >= 3) throw error\n this.onStatus(\"reconnecting\", error.message)\n await this.deps.sleep(1_000 * 2 ** this.retries++)\n if (this.cancelled) return\n await this.startAttempt()\n this.onStatus(\"reconnected\", \"Glasses relay restarted\")\n return\n } catch (failure) {\n // Cleanup failure retains ownership; never create another stream on uncertain state.\n if (this.hotspotTouched || this.nativeTouched || this.glassesTouched) {\n try {\n await this.cleanupAttempt()\n } catch (cleanupError) {\n if (!this.cancelled) this.onFailure(asError(cleanupError))\n return\n }\n }\n if (this.retries >= 3) {\n if (!this.cancelled) this.onFailure(asError(failure))\n return\n }\n }\n }\n })\n .finally(() => {\n this.restarting = false\n })\n }\n\n stop(): Promise<void> {\n this.cancel()\n if (this.stopping) return this.stopping\n this.listener?.remove()\n this.listener = null\n this.stopping = this.operation\n .catch(() => undefined)\n .then(async () => {\n await this.cleanupAttempt()\n this.release?.()\n this.release = null\n })\n .finally(() => {\n this.stopping = null\n })\n return this.stopping\n }\n\n private async cleanupAttempt(): Promise<void> {\n const failures: unknown[] = []\n const step = async (action: () => Promise<void>) => {\n try {\n await action()\n } catch (error) {\n failures.push(error)\n }\n }\n // Invalidate callbacks before intentionally stopping the glasses or dropping the network.\n const id = this.attemptId\n this.attemptId = null\n await step(async () => {\n this.micListener?.remove()\n this.micListener = null\n this.releaseMic?.()\n this.releaseMic = null\n })\n if (this.glassesTouched)\n await step(async () => {\n if (this.deps.connected()) await this.deps.stopGlasses()\n else this.deps.deferredStop()\n this.glassesTouched = false\n })\n if (this.nativeTouched && id)\n await step(async () => {\n await this.deps.native.stop(id)\n this.nativeTouched = false\n })\n if (this.hotspotTouched)\n await step(async () => {\n if (this.deps.connected()) {\n const result = await this.deps.hotspot(false)\n if (result.state !== \"disabled\") throw new Error(\"Glasses hotspot shutdown was not confirmed\")\n } else this.deps.deferredStop()\n this.hotspotTouched = false\n })\n if (failures.length) {\n this.attemptId = id // Retain the native attempt token for a subsequent cleanup retry.\n throw new Error(`Relay cleanup failed: ${failures.map((error) => asError(error).message).join(\"; \")}`)\n }\n }\n}\n\nfunction asError(error: unknown): Error {\n return error instanceof Error ? error : new Error(String(error))\n}\n\nexport function createManagedWebRtcRelay(\n options: RelayOptions,\n onStatus: (status: string, reason: string) => void,\n onFailure: (error: Error) => void,\n connected: () => boolean,\n deferredStop: () => void,\n): ManagedRelay {\n // Load only for managed WHIP. Hosts using direct SRT/RTMP need no relay module.\n const {requireNativeModule} = require(\"expo-modules-core\") as typeof import(\"expo-modules-core\")\n const {BgTimer} = require(\"../utils/timers\") as typeof import(\"../utils/timers\")\n const native = requireNativeModule<NativeRelay>(\"MentraGlassesMediaRelay\")\n const {Platform} = require(\"react-native\") as typeof import(\"react-native\")\n const {default: micSessionManager} = require(\"./MicSessionManager\") as typeof import(\"./MicSessionManager\")\n return new ManagedWebRtcRelay(options, onStatus, onFailure, {\n native,\n hotspot: (enabled) => BluetoothSdk.setHotspotState(enabled),\n startGlasses: (request) => BluetoothSdk.startStream(request),\n stopGlasses: () => BluetoothSdk.stopStream(),\n connected,\n deferredStop,\n now: Date.now,\n sleep: (ms) => new Promise((resolve) => BgTimer.setTimeout(resolve, ms)),\n acquire: acquireGlassesHotspot,\n ...(Platform.OS === \"android\" && typeof BluetoothSdk.setMicSourcePin === \"function\"\n ? {\n microphone: {\n acquire: () =>\n micSessionManager.acquire({\n owner: `engine:managed-relay:${options.streamId}`,\n source: \"glasses\",\n useCase: \"livestream\",\n }).release,\n subscribe: (listener) => BluetoothSdk.addListener(\"mic_pcm\", listener),\n },\n }\n : {}),\n })\n}\n"]}
@@ -10,7 +10,7 @@
10
10
  * in MicStateCoordinator, and the wire write lives below that again.
11
11
  */
12
12
  /** What an application is doing with the microphone. */
13
- export type MicUseCase = "voice_call" | "transcription" | "voice_assistant" | "diagnostic";
13
+ export type MicUseCase = "voice_call" | "transcription" | "voice_assistant" | "diagnostic" | "livestream";
14
14
  /** Which microphone the audio comes from. */
15
15
  export type MicSource = "glasses" | "phone";
16
16
  /** Subset of the `mic_tuning` wire payload. Fields left out keep firmware defaults. */
@@ -1 +1 @@
1
- {"version":3,"file":"micPolicy.d.ts","sourceRoot":"","sources":["../../src/services/micPolicy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,wDAAwD;AACxD,MAAM,MAAM,UAAU,GAAG,YAAY,GAAG,eAAe,GAAG,iBAAiB,GAAG,YAAY,CAAA;AAE1F,6CAA6C;AAC7C,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,OAAO,CAAA;AAE3C,uFAAuF;AACvF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,iCAAiC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,uCAAuC;IACvC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,oDAAoD;IACpD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,4EAA4E;AAC5E,eAAO,MAAM,OAAO,UAA+D,CAAA;AAEnF;;;;;GAKG;AACH,eAAO,MAAM,4BAA4B;;;;;;CAM/B,CAAA;AAEV;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAAC,gBAAgB,CAAC,CAU7E;AAED,+BAA+B;AAC/B,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,UAAU,CAAA;IACnB,MAAM,EAAE,SAAS,CAAA;CAClB,CAAA;AAED,6DAA6D;AAC7D,MAAM,MAAM,iBAAiB,GAAG;IAC9B,uFAAuF;IACvF,MAAM,EAAE,OAAO,CAAA;IACf,uFAAuF;IACvF,UAAU,EAAE,OAAO,CAAA;IACnB,+EAA+E;IAC/E,SAAS,EAAE,gBAAgB,GAAG,IAAI,CAAA;IAClC,gFAAgF;IAChF,YAAY,EAAE,OAAO,GAAG,IAAI,CAAA;CAC7B,CAAA;AAED,sEAAsE;AACtE,MAAM,MAAM,eAAe,GAAG;IAC5B;;;;OAIG;IACH,gBAAgB,EAAE,OAAO,CAAA;CAC1B,CAAA;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,qBAAqB,EAAE,MAAM,CAAC,UAAU,EAAE,gBAAgB,CAKtE,CAAA;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,0BAA0B,EAAE,MAAM,CAAC,UAAU,EAAE,OAAO,CAKlE,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAAwB,CAAA;AAEzE,yDAAyD;AACzD,eAAO,MAAM,aAAa,EAAE,SAAS,UAAU,EAK9C,CAAA;AAED,iFAAiF;AACjF,eAAO,MAAM,mBAAmB,YAAY,CAAA;AAE5C,kDAAkD;AAClD,eAAO,MAAM,qBAAqB,EAAE,SAAS,UAAU,EAAmB,CAAA;AAE1E;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,SAAS,cAAc,EAAE,EACnC,IAAI,EAAE,eAAe,GACpB,iBAAiB,CAwBnB"}
1
+ {"version":3,"file":"micPolicy.d.ts","sourceRoot":"","sources":["../../src/services/micPolicy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,wDAAwD;AACxD,MAAM,MAAM,UAAU,GAAG,YAAY,GAAG,eAAe,GAAG,iBAAiB,GAAG,YAAY,GAAG,YAAY,CAAA;AAEzG,6CAA6C;AAC7C,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,OAAO,CAAA;AAE3C,uFAAuF;AACvF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,iCAAiC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,uCAAuC;IACvC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,oDAAoD;IACpD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,4EAA4E;AAC5E,eAAO,MAAM,OAAO,UAA+D,CAAA;AAEnF;;;;;GAKG;AACH,eAAO,MAAM,4BAA4B;;;;;;CAM/B,CAAA;AAEV;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAAC,gBAAgB,CAAC,CAU7E;AAED,+BAA+B;AAC/B,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,UAAU,CAAA;IACnB,MAAM,EAAE,SAAS,CAAA;CAClB,CAAA;AAED,6DAA6D;AAC7D,MAAM,MAAM,iBAAiB,GAAG;IAC9B,uFAAuF;IACvF,MAAM,EAAE,OAAO,CAAA;IACf,uFAAuF;IACvF,UAAU,EAAE,OAAO,CAAA;IACnB,+EAA+E;IAC/E,SAAS,EAAE,gBAAgB,GAAG,IAAI,CAAA;IAClC,gFAAgF;IAChF,YAAY,EAAE,OAAO,GAAG,IAAI,CAAA;CAC7B,CAAA;AAED,sEAAsE;AACtE,MAAM,MAAM,eAAe,GAAG;IAC5B;;;;OAIG;IACH,gBAAgB,EAAE,OAAO,CAAA;CAC1B,CAAA;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,qBAAqB,EAAE,MAAM,CAAC,UAAU,EAAE,gBAAgB,CAMtE,CAAA;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,0BAA0B,EAAE,MAAM,CAAC,UAAU,EAAE,OAAO,CAMlE,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAAwB,CAAA;AAEzE,yDAAyD;AACzD,eAAO,MAAM,aAAa,EAAE,SAAS,UAAU,EAM9C,CAAA;AAED,iFAAiF;AACjF,eAAO,MAAM,mBAAmB,YAAY,CAAA;AAE5C,kDAAkD;AAClD,eAAO,MAAM,qBAAqB,EAAE,SAAS,UAAU,EAAiC,CAAA;AAExF;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,SAAS,cAAc,EAAE,EACnC,IAAI,EAAE,eAAe,GACpB,iBAAiB,CAwBnB"}
@@ -65,6 +65,7 @@ export const MIC_USE_CASE_PROFILES = {
65
65
  transcription: {},
66
66
  voice_assistant: {},
67
67
  diagnostic: {},
68
+ livestream: {}, // Preserve the configured capture gain; there is no call return audio.
68
69
  };
69
70
  /**
70
71
  * Use cases that run the center-mic loudness gate ("Barrier").
@@ -82,6 +83,7 @@ export const MIC_USE_CASE_LOUDNESS_GATE = {
82
83
  transcription: false,
83
84
  voice_assistant: false,
84
85
  diagnostic: false,
86
+ livestream: false,
85
87
  };
86
88
  /**
87
89
  * Packages allowed to hold a `voice_call` session.
@@ -96,11 +98,12 @@ export const MIC_USE_CASES = [
96
98
  "transcription",
97
99
  "voice_assistant",
98
100
  "diagnostic",
101
+ "livestream",
99
102
  ];
100
103
  /** Owners of engine-internal sessions. Miniapps cannot claim these use cases. */
101
104
  export const ENGINE_OWNER_PREFIX = "engine:";
102
105
  /** Use cases only engine features may acquire. */
103
- export const ENGINE_ONLY_USE_CASES = ["diagnostic"];
106
+ export const ENGINE_ONLY_USE_CASES = ["diagnostic", "livestream"];
104
107
  /**
105
108
  * Resolve every live session into one hardware state.
106
109
  *
@@ -1 +1 @@
1
- {"version":3,"file":"micPolicy.js","sourceRoot":"","sources":["../../src/services/micPolicy.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Microphone use-case policy.\n *\n * Applications say what they are doing (\"voice_call\"); the engine decides what\n * that requires of the hardware. Nothing above this file knows that a voice\n * call means ADC index 14, and no miniapp can ask for a gain directly.\n *\n * Pure: every function here is a function of the live session set. The lease\n * bookkeeping lives in MicSessionManager, the merge with OS preferences lives\n * in MicStateCoordinator, and the wire write lives below that again.\n */\n\n/** What an application is doing with the microphone. */\nexport type MicUseCase = \"voice_call\" | \"transcription\" | \"voice_assistant\" | \"diagnostic\"\n\n/** Which microphone the audio comes from. */\nexport type MicSource = \"glasses\" | \"phone\"\n\n/** Subset of the `mic_tuning` wire payload. Fields left out keep firmware defaults. */\nexport type MicTuningProfile = {\n /** codec_adc_vol index, 0-15. */\n gain?: number\n /** Center-mic RMS to open the gate. */\n open?: number\n /** Center-mic RMS to close it again. */\n close?: number\n /** Open threshold while the speaker is elevated. */\n sp_open?: number\n /** Close threshold while the speaker is elevated. */\n sp_close?: number\n}\n\n/** `codec_adc_vol[]`: index to dB. Index 0 is mute and is never offered. */\nexport const GAIN_DB = [-99, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 32]\n\n/**\n * Mirrored from `center_mic_vad_get_default_config` and `CODEC_SADC_VOL`.\n *\n * The four RMS numbers are measured *after* the ADC gain, so they are only\n * meaningful next to the gain they were calibrated at, which is index 15.\n */\nexport const MIC_TUNING_FIRMWARE_DEFAULTS = {\n gain: 15,\n open: 1350,\n close: 945,\n sp_open: 2900,\n sp_close: 1600,\n} as const\n\n/**\n * Re-express the firmware's thresholds at a different gain.\n *\n * The gate compares post-gain RMS, so moving the gain without moving the\n * thresholds silently changes what the gate means: at -8 dB the wearer has to\n * be 2.5x louder to clear a number that was chosen for +32 dB. Scaling both by\n * the same factor keeps the *acoustic* trip point the profile was tuned for,\n * for the wearer and for speaker leak alike.\n */\nexport function scaleMicTuningToGain(gain: number): Required<MicTuningProfile> {\n const index = Math.min(GAIN_DB.length - 1, Math.max(1, Math.round(gain)))\n const factor = Math.pow(10, (GAIN_DB[index]! - GAIN_DB[MIC_TUNING_FIRMWARE_DEFAULTS.gain]!) / 20)\n return {\n gain: index,\n open: Math.round(MIC_TUNING_FIRMWARE_DEFAULTS.open * factor),\n close: Math.round(MIC_TUNING_FIRMWARE_DEFAULTS.close * factor),\n sp_open: Math.round(MIC_TUNING_FIRMWARE_DEFAULTS.sp_open * factor),\n sp_close: Math.round(MIC_TUNING_FIRMWARE_DEFAULTS.sp_close * factor),\n }\n}\n\n/** A live microphone lease. */\nexport type MicSessionSpec = {\n useCase: MicUseCase\n source: MicSource\n}\n\n/** What the hardware should do, given every live session. */\nexport type ResolvedMicPolicy = {\n /** Someone needs a continuous raw-PCM timeline, which also forces hardware VAD off. */\n rawPcm: boolean\n /** Pin the Bluetooth SDK to the glasses microphone. Only the glasses can be pinned. */\n pinGlasses: boolean\n /** Tuning override for the glasses, or null to leave the OS value in force. */\n micTuning: MicTuningProfile | null\n /** Run the center-mic loudness gate, or null to leave the OS value in force. */\n loudnessGate: boolean | null\n}\n\n/** Platform facts the policy cannot infer from the sessions alone. */\nexport type MicPlatformCaps = {\n /**\n * Whether this platform can take the wearer's voice off the glasses as raw\n * PCM over BLE LC3. False on iOS, which has no `setMicSourcePin` and never\n * selects the `ble-lc3` uplink.\n */\n glassesPcmUplink: boolean\n}\n\n/**\n * Mentra Live ships CODEC_SADC_VOL = 15, the last entry of codec_adc_vol[] and\n * +32 dB. That table steps 2 dB at a time everywhere except the final step,\n * which jumps 6 dB from +26, so the default sits at the ceiling one oversized\n * step above everything else. It suits a wearer dictating to a transcription\n * miniapp across a room; it clips a wearer talking into a Teams call.\n *\n * Index 14 is +26 dB: one step down, and the step that removes the table's\n * anomalous jump. A same-voice sweep showed 15 rails hard (2.5–4.2% clip);\n * 14/13/12 still kiss the rail on syllable tips. 13 is +24 dB, the middle\n * of that band, while we listen for loudness vs residual clip.\n *\n * The thresholds ride along with the gain rather than being stated here, so\n * changing the index above cannot leave the gate calibrated for a level the\n * ADC no longer produces.\n */\nexport const MIC_USE_CASE_PROFILES: Record<MicUseCase, MicTuningProfile> = {\n voice_call: scaleMicTuningToGain(13),\n transcription: {},\n voice_assistant: {},\n diagnostic: {},\n}\n\n/**\n * Use cases that run the center-mic loudness gate (\"Barrier\").\n *\n * A voice call is the one place it earns its keep. There is no echo canceller\n * on the LC3 uplink — `MENTRA_LC3_SPEECH_PROCESSING` is 0 and the capture path\n * ignores the playback reference — so without a gate the far end hears itself\n * through the wearer's speaker. Barrier is the safe half of the firmware's two\n * gates: it zeroes a quiet frame but never stops transmitting, where VAD drops\n * the stream outright. Paired with the speaker-elevated thresholds above, that\n * suppresses leak while the far end is talking and costs the wearer nothing.\n */\nexport const MIC_USE_CASE_LOUDNESS_GATE: Record<MicUseCase, boolean> = {\n voice_call: true,\n transcription: false,\n voice_assistant: false,\n diagnostic: false,\n}\n\n/**\n * Packages allowed to hold a `voice_call` session.\n *\n * Which app may make a voice call is policy, so it lives beside the profiles\n * rather than in the request handler that enforces it.\n */\nexport const VOICE_CALL_PACKAGES: readonly string[] = [\"com.mentra.call\"]\n\n/** Every use case, for validating an inbound request. */\nexport const MIC_USE_CASES: readonly MicUseCase[] = [\n \"voice_call\",\n \"transcription\",\n \"voice_assistant\",\n \"diagnostic\",\n]\n\n/** Owners of engine-internal sessions. Miniapps cannot claim these use cases. */\nexport const ENGINE_OWNER_PREFIX = \"engine:\"\n\n/** Use cases only engine features may acquire. */\nexport const ENGINE_ONLY_USE_CASES: readonly MicUseCase[] = [\"diagnostic\"]\n\n/**\n * Resolve every live session into one hardware state.\n *\n * On a platform without a glasses PCM uplink a glasses session is a lease and\n * nothing more: it satisfies ownership checks so an iOS call can still be\n * modelled the same way, but it claims no PCM, pins nothing, and applies no\n * gain, because the wearer's voice does not reach the call through the BES\n * BLE path there.\n */\nexport function resolveMicPolicy(\n sessions: readonly MicSessionSpec[],\n caps: MicPlatformCaps,\n): ResolvedMicPolicy {\n const effective = caps.glassesPcmUplink ? sessions : sessions.filter((s) => s.source !== \"glasses\")\n\n let pinGlasses = false\n let loudnessGate = false\n let winning: MicTuningProfile | null = null\n for (const session of effective) {\n if (session.source !== \"glasses\") continue\n pinGlasses = true\n if (MIC_USE_CASE_LOUDNESS_GATE[session.useCase]) loudnessGate = true\n const profile = MIC_USE_CASE_PROFILES[session.useCase]\n if (typeof profile?.gain !== \"number\") continue\n // Lowest index wins: clipping is the irreversible failure, a slightly\n // quiet assistant is not. The whole profile travels with it, because its\n // thresholds are only meaningful at its own gain.\n if (winning === null || profile.gain < winning.gain!) winning = profile\n }\n\n return {\n rawPcm: effective.length > 0,\n pinGlasses,\n micTuning: winning,\n loudnessGate: pinGlasses ? loudnessGate : null,\n }\n}\n"]}
1
+ {"version":3,"file":"micPolicy.js","sourceRoot":"","sources":["../../src/services/micPolicy.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Microphone use-case policy.\n *\n * Applications say what they are doing (\"voice_call\"); the engine decides what\n * that requires of the hardware. Nothing above this file knows that a voice\n * call means ADC index 14, and no miniapp can ask for a gain directly.\n *\n * Pure: every function here is a function of the live session set. The lease\n * bookkeeping lives in MicSessionManager, the merge with OS preferences lives\n * in MicStateCoordinator, and the wire write lives below that again.\n */\n\n/** What an application is doing with the microphone. */\nexport type MicUseCase = \"voice_call\" | \"transcription\" | \"voice_assistant\" | \"diagnostic\" | \"livestream\"\n\n/** Which microphone the audio comes from. */\nexport type MicSource = \"glasses\" | \"phone\"\n\n/** Subset of the `mic_tuning` wire payload. Fields left out keep firmware defaults. */\nexport type MicTuningProfile = {\n /** codec_adc_vol index, 0-15. */\n gain?: number\n /** Center-mic RMS to open the gate. */\n open?: number\n /** Center-mic RMS to close it again. */\n close?: number\n /** Open threshold while the speaker is elevated. */\n sp_open?: number\n /** Close threshold while the speaker is elevated. */\n sp_close?: number\n}\n\n/** `codec_adc_vol[]`: index to dB. Index 0 is mute and is never offered. */\nexport const GAIN_DB = [-99, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 32]\n\n/**\n * Mirrored from `center_mic_vad_get_default_config` and `CODEC_SADC_VOL`.\n *\n * The four RMS numbers are measured *after* the ADC gain, so they are only\n * meaningful next to the gain they were calibrated at, which is index 15.\n */\nexport const MIC_TUNING_FIRMWARE_DEFAULTS = {\n gain: 15,\n open: 1350,\n close: 945,\n sp_open: 2900,\n sp_close: 1600,\n} as const\n\n/**\n * Re-express the firmware's thresholds at a different gain.\n *\n * The gate compares post-gain RMS, so moving the gain without moving the\n * thresholds silently changes what the gate means: at -8 dB the wearer has to\n * be 2.5x louder to clear a number that was chosen for +32 dB. Scaling both by\n * the same factor keeps the *acoustic* trip point the profile was tuned for,\n * for the wearer and for speaker leak alike.\n */\nexport function scaleMicTuningToGain(gain: number): Required<MicTuningProfile> {\n const index = Math.min(GAIN_DB.length - 1, Math.max(1, Math.round(gain)))\n const factor = Math.pow(10, (GAIN_DB[index]! - GAIN_DB[MIC_TUNING_FIRMWARE_DEFAULTS.gain]!) / 20)\n return {\n gain: index,\n open: Math.round(MIC_TUNING_FIRMWARE_DEFAULTS.open * factor),\n close: Math.round(MIC_TUNING_FIRMWARE_DEFAULTS.close * factor),\n sp_open: Math.round(MIC_TUNING_FIRMWARE_DEFAULTS.sp_open * factor),\n sp_close: Math.round(MIC_TUNING_FIRMWARE_DEFAULTS.sp_close * factor),\n }\n}\n\n/** A live microphone lease. */\nexport type MicSessionSpec = {\n useCase: MicUseCase\n source: MicSource\n}\n\n/** What the hardware should do, given every live session. */\nexport type ResolvedMicPolicy = {\n /** Someone needs a continuous raw-PCM timeline, which also forces hardware VAD off. */\n rawPcm: boolean\n /** Pin the Bluetooth SDK to the glasses microphone. Only the glasses can be pinned. */\n pinGlasses: boolean\n /** Tuning override for the glasses, or null to leave the OS value in force. */\n micTuning: MicTuningProfile | null\n /** Run the center-mic loudness gate, or null to leave the OS value in force. */\n loudnessGate: boolean | null\n}\n\n/** Platform facts the policy cannot infer from the sessions alone. */\nexport type MicPlatformCaps = {\n /**\n * Whether this platform can take the wearer's voice off the glasses as raw\n * PCM over BLE LC3. False on iOS, which has no `setMicSourcePin` and never\n * selects the `ble-lc3` uplink.\n */\n glassesPcmUplink: boolean\n}\n\n/**\n * Mentra Live ships CODEC_SADC_VOL = 15, the last entry of codec_adc_vol[] and\n * +32 dB. That table steps 2 dB at a time everywhere except the final step,\n * which jumps 6 dB from +26, so the default sits at the ceiling one oversized\n * step above everything else. It suits a wearer dictating to a transcription\n * miniapp across a room; it clips a wearer talking into a Teams call.\n *\n * Index 14 is +26 dB: one step down, and the step that removes the table's\n * anomalous jump. A same-voice sweep showed 15 rails hard (2.5–4.2% clip);\n * 14/13/12 still kiss the rail on syllable tips. 13 is +24 dB, the middle\n * of that band, while we listen for loudness vs residual clip.\n *\n * The thresholds ride along with the gain rather than being stated here, so\n * changing the index above cannot leave the gate calibrated for a level the\n * ADC no longer produces.\n */\nexport const MIC_USE_CASE_PROFILES: Record<MicUseCase, MicTuningProfile> = {\n voice_call: scaleMicTuningToGain(13),\n transcription: {},\n voice_assistant: {},\n diagnostic: {},\n livestream: {}, // Preserve the configured capture gain; there is no call return audio.\n}\n\n/**\n * Use cases that run the center-mic loudness gate (\"Barrier\").\n *\n * A voice call is the one place it earns its keep. There is no echo canceller\n * on the LC3 uplink — `MENTRA_LC3_SPEECH_PROCESSING` is 0 and the capture path\n * ignores the playback reference — so without a gate the far end hears itself\n * through the wearer's speaker. Barrier is the safe half of the firmware's two\n * gates: it zeroes a quiet frame but never stops transmitting, where VAD drops\n * the stream outright. Paired with the speaker-elevated thresholds above, that\n * suppresses leak while the far end is talking and costs the wearer nothing.\n */\nexport const MIC_USE_CASE_LOUDNESS_GATE: Record<MicUseCase, boolean> = {\n voice_call: true,\n transcription: false,\n voice_assistant: false,\n diagnostic: false,\n livestream: false,\n}\n\n/**\n * Packages allowed to hold a `voice_call` session.\n *\n * Which app may make a voice call is policy, so it lives beside the profiles\n * rather than in the request handler that enforces it.\n */\nexport const VOICE_CALL_PACKAGES: readonly string[] = [\"com.mentra.call\"]\n\n/** Every use case, for validating an inbound request. */\nexport const MIC_USE_CASES: readonly MicUseCase[] = [\n \"voice_call\",\n \"transcription\",\n \"voice_assistant\",\n \"diagnostic\",\n \"livestream\",\n]\n\n/** Owners of engine-internal sessions. Miniapps cannot claim these use cases. */\nexport const ENGINE_OWNER_PREFIX = \"engine:\"\n\n/** Use cases only engine features may acquire. */\nexport const ENGINE_ONLY_USE_CASES: readonly MicUseCase[] = [\"diagnostic\", \"livestream\"]\n\n/**\n * Resolve every live session into one hardware state.\n *\n * On a platform without a glasses PCM uplink a glasses session is a lease and\n * nothing more: it satisfies ownership checks so an iOS call can still be\n * modelled the same way, but it claims no PCM, pins nothing, and applies no\n * gain, because the wearer's voice does not reach the call through the BES\n * BLE path there.\n */\nexport function resolveMicPolicy(\n sessions: readonly MicSessionSpec[],\n caps: MicPlatformCaps,\n): ResolvedMicPolicy {\n const effective = caps.glassesPcmUplink ? sessions : sessions.filter((s) => s.source !== \"glasses\")\n\n let pinGlasses = false\n let loudnessGate = false\n let winning: MicTuningProfile | null = null\n for (const session of effective) {\n if (session.source !== \"glasses\") continue\n pinGlasses = true\n if (MIC_USE_CASE_LOUDNESS_GATE[session.useCase]) loudnessGate = true\n const profile = MIC_USE_CASE_PROFILES[session.useCase]\n if (typeof profile?.gain !== \"number\") continue\n // Lowest index wins: clipping is the irreversible failure, a slightly\n // quiet assistant is not. The whole profile travels with it, because its\n // thresholds are only meaningful at its own gain.\n if (winning === null || profile.gain < winning.gain!) winning = profile\n }\n\n return {\n rawPcm: effective.length > 0,\n pinGlasses,\n micTuning: winning,\n loudnessGate: pinGlasses ? loudnessGate : null,\n }\n}\n"]}
@@ -0,0 +1,3 @@
1
+ /** Encode decoded LC3 PCM for native sinks. Hermes has btoa, but no Node Buffer. */
2
+ export declare function pcmToBase64(pcm: ArrayBuffer): string;
3
+ //# sourceMappingURL=pcmToBase64.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pcmToBase64.d.ts","sourceRoot":"","sources":["../../src/utils/pcmToBase64.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,wBAAgB,WAAW,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,CAQpD"}
@@ -0,0 +1,11 @@
1
+ /** Encode decoded LC3 PCM for native sinks. Hermes has btoa, but no Node Buffer. */
2
+ export function pcmToBase64(pcm) {
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
+ }
11
+ //# sourceMappingURL=pcmToBase64.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pcmToBase64.js","sourceRoot":"","sources":["../../src/utils/pcmToBase64.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,MAAM,UAAU,WAAW,CAAC,GAAgB;IAC1C,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,GAAG,CAAC,CAAA;IACjC,IAAI,MAAM,GAAG,EAAE,CAAA;IACf,MAAM,IAAI,GAAG,MAAM,CAAA;IACnB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC;QAC5C,MAAM,IAAI,MAAM,CAAC,YAAY,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,CAAA;IAC/D,CAAC;IACD,OAAO,IAAI,CAAC,MAAM,CAAC,CAAA;AACrB,CAAC","sourcesContent":["/** Encode decoded LC3 PCM for native sinks. Hermes has btoa, but no Node Buffer. */\nexport function pcmToBase64(pcm: ArrayBuffer): string {\n const bytes = new Uint8Array(pcm)\n let binary = \"\"\n const step = 0x8000\n for (let i = 0; i < bytes.length; i += step) {\n binary += String.fromCharCode(...bytes.subarray(i, i + step))\n }\n return btoa(binary)\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/engine",
3
- "version": "3.2.1-dev.288",
3
+ "version": "3.2.1-dev.291",
4
4
  "description": "Mentra Engine — miniapp registry, WebView bridge, and miniapp store",
5
5
  "main": "build/index.js",
6
6
  "react-native": "src/index.ts",
@@ -97,13 +97,13 @@
97
97
  "registry": "https://registry.npmjs.org/"
98
98
  },
99
99
  "dependencies": {
100
- "@mentra/acs-meeting": "3.2.1-dev.288",
101
- "@mentra/glasses-media": "3.2.1-dev.288",
102
- "@mentra/bluetooth-sdk": "3.2.1-dev.288",
103
- "@mentra/cloud-client": "3.2.1-dev.288",
104
- "@mentra/cloud-protocol": "3.2.1-dev.288",
105
- "@mentra/crust": "3.2.1-dev.288",
106
- "@mentra/miniapp": "3.2.1-dev.288",
100
+ "@mentra/acs-meeting": "3.2.1-dev.291",
101
+ "@mentra/glasses-media": "3.2.1-dev.291",
102
+ "@mentra/bluetooth-sdk": "3.2.1-dev.291",
103
+ "@mentra/cloud-client": "3.2.1-dev.291",
104
+ "@mentra/cloud-protocol": "3.2.1-dev.291",
105
+ "@mentra/crust": "3.2.1-dev.291",
106
+ "@mentra/miniapp": "3.2.1-dev.291",
107
107
  "buffer": "^6.0.3",
108
108
  "events": "^3.3.0",
109
109
  "react-native-marked": "8.1.1",
@@ -12,9 +12,9 @@ export interface EngineReleaseMetadata {
12
12
  export const ENGINE_RELEASE_METADATA: Readonly<EngineReleaseMetadata> = Object.freeze({
13
13
  "schemaVersion": 1,
14
14
  "familyBaseVersion": "3.2.1",
15
- "releaseIdentity": "3.2.1-dev.288",
16
- "releaseSetId": "mentra-3.2.1-dev.288",
17
- "sourceCommit": "f46b903eda259d0f124f23e619e7bdbee97aa700",
18
- "otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-dev.288.json",
19
- "otaManifestSha256": "08b933880e0dc904b137cc700aa82bb2a86f593bb7b030f6cc0e546888a4fcd2"
15
+ "releaseIdentity": "3.2.1-dev.291",
16
+ "releaseSetId": "mentra-3.2.1-dev.291",
17
+ "sourceCommit": "68801c65eed5739f4191b07bb0b63d3802f328cb",
18
+ "otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-dev.291.json",
19
+ "otaManifestSha256": "12704a1c72b5001c3001ad2935fd68d67cc8cd36d63776780f7f031a375124a7"
20
20
  })
@@ -15,11 +15,13 @@ import micSessionManager, {type MicSession} from "./MicSessionManager"
15
15
  import {ENGINE_OWNER_PREFIX} from "./micPolicy"
16
16
  import {SETTINGS, useSettingsStore} from "../stores/settings"
17
17
  import {Pcm16LevelMeter, pcm16WindowStats} from "../utils/pcm16"
18
+ import {pcmToBase64} from "../utils/pcmToBase64"
18
19
  import {softapTrace, softapTraceFailure, softapTraceId} from "../utils/softapTrace"
19
20
  import {ACS_CALL_MIC, type AcsAudioSource, type ResolvedAudioSource, type SourceReason} from "./acsAudioSource"
20
21
  import type {SoftapProgress} from "./SoftapCallTransport"
21
22
 
22
23
  export {ACS_CALL_MIC}
24
+ export {pcmToBase64}
23
25
  export type {ResolvedAudioSource, SourceReason}
24
26
 
25
27
  type MeetingPhase = "idle" | "connecting" | "lobby" | "connected" | "disconnected" | "error"
@@ -550,24 +552,6 @@ const MIC_UPLINK_SWEEP_LOG_INTERVAL_MS = 1000
550
552
  /** A 50 ms LC3 frame arriving more than this late is a missed beat, not jitter. */
551
553
  const MIC_GAP_WARN_MS = 90
552
554
 
553
- /**
554
- * Encode one microphone buffer for `pushOutgoingPcm`.
555
- *
556
- * Hermes has no Node `Buffer`. Using it here is how a live SoftAP call selected `ble-lc3`,
557
- * pinned the glasses, and still sent Teams a minute of silence: every `mic_pcm` event threw
558
- * `Property 'Buffer' doesn't exist` before native saw a byte. `btoa` is what React Native
559
- * actually has.
560
- */
561
- export function pcmToBase64(pcm: ArrayBuffer): string {
562
- const bytes = new Uint8Array(pcm)
563
- let binary = ""
564
- const step = 0x8000
565
- for (let i = 0; i < bytes.length; i += step) {
566
- binary += String.fromCharCode(...bytes.subarray(i, i + step))
567
- }
568
- return btoa(binary)
569
- }
570
-
571
555
  class AcsMeetingService {
572
556
  private owner: string | null = null
573
557
  private pcmStreamId: string | null = null