@mentra/engine 3.2.0-dev.221 → 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/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts +2 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/services/AcsMeetingService.d.ts +360 -10
- package/build/services/AcsMeetingService.d.ts.map +1 -1
- package/build/services/AcsMeetingService.js +836 -19
- package/build/services/AcsMeetingService.js.map +1 -1
- package/build/services/AudioPlaybackService.d.ts +6 -0
- package/build/services/AudioPlaybackService.d.ts.map +1 -1
- package/build/services/AudioPlaybackService.js +4 -3
- package/build/services/AudioPlaybackService.js.map +1 -1
- package/build/services/GlassesMicProbe.d.ts +78 -0
- package/build/services/GlassesMicProbe.d.ts.map +1 -0
- package/build/services/GlassesMicProbe.js +258 -0
- package/build/services/GlassesMicProbe.js.map +1 -0
- package/build/services/LocalMiniappRuntime.d.ts +83 -0
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +714 -39
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/MentraJSLogPipeline.d.ts +39 -0
- package/build/services/MentraJSLogPipeline.d.ts.map +1 -1
- package/build/services/MentraJSLogPipeline.js +59 -3
- package/build/services/MentraJSLogPipeline.js.map +1 -1
- package/build/services/MentraJSRouter.d.ts +1 -1
- package/build/services/MentraJSRouter.d.ts.map +1 -1
- package/build/services/MentraJSRouter.js +2 -2
- package/build/services/MentraJSRouter.js.map +1 -1
- package/build/services/MicStateCoordinator.d.ts +22 -0
- package/build/services/MicStateCoordinator.d.ts.map +1 -1
- package/build/services/MicStateCoordinator.js +34 -3
- package/build/services/MicStateCoordinator.js.map +1 -1
- package/build/services/PhoneStreamCoordinator.d.ts +8 -0
- package/build/services/PhoneStreamCoordinator.d.ts.map +1 -1
- package/build/services/PhoneStreamCoordinator.js +2 -0
- package/build/services/PhoneStreamCoordinator.js.map +1 -1
- package/build/services/SoftapCallTransport.d.ts +365 -0
- package/build/services/SoftapCallTransport.d.ts.map +1 -0
- package/build/services/SoftapCallTransport.js +722 -0
- package/build/services/SoftapCallTransport.js.map +1 -0
- package/build/services/SoftapCleanupBarrier.d.ts +45 -0
- package/build/services/SoftapCleanupBarrier.d.ts.map +1 -0
- package/build/services/SoftapCleanupBarrier.js +51 -0
- package/build/services/SoftapCleanupBarrier.js.map +1 -0
- package/build/utils/pcm16.d.ts +35 -0
- package/build/utils/pcm16.d.ts.map +1 -0
- package/build/utils/pcm16.js +85 -0
- package/build/utils/pcm16.js.map +1 -0
- package/build/utils/softapTrace.d.ts +39 -0
- package/build/utils/softapTrace.d.ts.map +1 -0
- package/build/utils/softapTrace.js +124 -0
- package/build/utils/softapTrace.js.map +1 -0
- package/package.json +7 -7
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/index.ts +2 -0
- package/src/services/AcsMeetingService.ts +956 -22
- package/src/services/AudioPlaybackService.ts +12 -3
- package/src/services/GlassesMicProbe.ts +300 -0
- package/src/services/LocalMiniappRuntime.ts +775 -42
- package/src/services/MentraJSLogPipeline.ts +70 -3
- package/src/services/MentraJSRouter.ts +2 -2
- package/src/services/MicStateCoordinator.ts +35 -3
- package/src/services/PhoneStreamCoordinator.ts +10 -0
- package/src/services/SoftapCallTransport.ts +928 -0
- package/src/services/SoftapCleanupBarrier.ts +72 -0
- package/src/utils/pcm16.ts +93 -0
- package/src/utils/softapTrace.ts +133 -0
|
@@ -60,6 +60,12 @@ export interface AudioStreamOpenRequest {
|
|
|
60
60
|
channels: number
|
|
61
61
|
volume?: number
|
|
62
62
|
stopOtherAudio?: boolean
|
|
63
|
+
/**
|
|
64
|
+
* Playout headroom in ms, which is also this stream's floor latency because the native track
|
|
65
|
+
* fills to its buffer and stays there. Omit it for clip playback; a realtime caller passes a
|
|
66
|
+
* small value and accepts the thinner cushion. Android only.
|
|
67
|
+
*/
|
|
68
|
+
jitterMs?: number
|
|
63
69
|
/**
|
|
64
70
|
* Called when the stream ends for any reason (drained after close, aborted,
|
|
65
71
|
* interrupted by other audio, or native error).
|
|
@@ -659,8 +665,11 @@ class AudioPlaybackService {
|
|
|
659
665
|
* following the media route such as A2DP to connected glasses.
|
|
660
666
|
*/
|
|
661
667
|
public async openStream(request: AudioStreamOpenRequest): Promise<void> {
|
|
662
|
-
const {streamId, appId, sampleRate, channels, volume = 1.0, stopOtherAudio = true} = request
|
|
663
|
-
console.log(
|
|
668
|
+
const {streamId, appId, sampleRate, channels, volume = 1.0, stopOtherAudio = true, jitterMs} = request
|
|
669
|
+
console.log(
|
|
670
|
+
`AUDIO: Stream open ${streamId} from ${appId}: rate=${sampleRate} ch=${channels}` +
|
|
671
|
+
(jitterMs === undefined ? "" : ` jitterMs=${jitterMs}`),
|
|
672
|
+
)
|
|
664
673
|
|
|
665
674
|
await this.ensureAudioModeConfigured()
|
|
666
675
|
await this.prewarmAudioRouteIfCold()
|
|
@@ -673,7 +682,7 @@ class AudioPlaybackService {
|
|
|
673
682
|
await this.abortAllStreams(streamId)
|
|
674
683
|
}
|
|
675
684
|
|
|
676
|
-
await BluetoothSdk.pcmStreamOpen(streamId, sampleRate, channels, Math.max(0, Math.min(1, volume)))
|
|
685
|
+
await BluetoothSdk.pcmStreamOpen(streamId, sampleRate, channels, Math.max(0, Math.min(1, volume)), jitterMs)
|
|
677
686
|
this.releaseTailUplinkSuppressions()
|
|
678
687
|
|
|
679
688
|
const stream: StreamState = {
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GlassesMicProbe — on-device level meter for the Bluetooth LC3 microphone.
|
|
3
|
+
*
|
|
4
|
+
* Pins the SDK to the glasses microphone exactly the way a Mentra Call does, then reports the
|
|
5
|
+
* decoded PCM level once a second as `[MIC_PROBE]` log lines, so a soak can tell "the glasses
|
|
6
|
+
* mic is analog-quiet" apart from "the uplink dropped it". Optionally keeps an A2DP PCM stream
|
|
7
|
+
* open to the glasses at the same time (silence or a tone), which is the duplex condition a
|
|
8
|
+
* Teams call puts the BES in: far-end audio playing while the LC3 mic is live.
|
|
9
|
+
*
|
|
10
|
+
* Dev tooling. Reachable from `com.mentra://test/mic-probe`.
|
|
11
|
+
*/
|
|
12
|
+
import BluetoothSdk from "@mentra/bluetooth-sdk/internal"
|
|
13
|
+
|
|
14
|
+
import audioPlaybackService from "./AudioPlaybackService"
|
|
15
|
+
import micStateCoordinator from "./MicStateCoordinator"
|
|
16
|
+
import {SETTINGS, useSettingsStore} from "../stores/settings"
|
|
17
|
+
import {summarizePcm16} from "../utils/pcm16"
|
|
18
|
+
import {BgTimer} from "../utils/timers"
|
|
19
|
+
|
|
20
|
+
export type MicProbeA2dpMode = "none" | "silence" | "tone"
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Which microphone to meter. `glasses` is the call path (pinned, LC3 over BLE). `phone` is a
|
|
24
|
+
* control: same meter on the phone's own microphone, so a flat glasses trace can be told apart
|
|
25
|
+
* from a quiet room.
|
|
26
|
+
*/
|
|
27
|
+
export type MicProbeSource = "glasses" | "phone"
|
|
28
|
+
|
|
29
|
+
export type MicProbeOptions = {
|
|
30
|
+
/** How long to run before stopping on its own. */
|
|
31
|
+
durationMs: number
|
|
32
|
+
/** What to play to the glasses over A2DP while the mic is live. */
|
|
33
|
+
a2dp: MicProbeA2dpMode
|
|
34
|
+
/** Peak amplitude of the tone as a fraction of full scale (0..1). */
|
|
35
|
+
toneLevel?: number
|
|
36
|
+
/** Microphone under test. Defaults to `glasses`. */
|
|
37
|
+
source?: MicProbeSource
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const A2DP_MODES: MicProbeA2dpMode[] = ["none", "silence", "tone"]
|
|
41
|
+
const SOURCES: MicProbeSource[] = ["glasses", "phone"]
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parse `com.mentra://test/mic-probe?seconds=&a2dp=&level=` query values.
|
|
45
|
+
*
|
|
46
|
+
* adb/shell often backslash-escapes `&`, so `seconds=15\&a2dp=none` arrives as
|
|
47
|
+
* `seconds=15\`. Digits-only parsing is what keeps that from falling back to 20s.
|
|
48
|
+
*/
|
|
49
|
+
export function parseMicProbeParams(params: Record<string, string | undefined | string[]>): MicProbeOptions {
|
|
50
|
+
const raw = (key: string) => {
|
|
51
|
+
const value = params[key]
|
|
52
|
+
return String(Array.isArray(value) ? value[0] : (value ?? ""))
|
|
53
|
+
}
|
|
54
|
+
const seconds = Number.parseInt(raw("seconds").replace(/[^\d]/g, ""), 10)
|
|
55
|
+
const durationMs = (Number.isFinite(seconds) && seconds > 0 ? Math.min(600, seconds) : 20) * 1000
|
|
56
|
+
const a2dpRaw = raw("a2dp")
|
|
57
|
+
const a2dp = (A2DP_MODES.includes(a2dpRaw as MicProbeA2dpMode) ? a2dpRaw : "none") as MicProbeA2dpMode
|
|
58
|
+
const toneLevel = Number.parseFloat(raw("level").replace(/[^\d.]/g, ""))
|
|
59
|
+
const sourceRaw = raw("mic")
|
|
60
|
+
const source = (SOURCES.includes(sourceRaw as MicProbeSource) ? sourceRaw : "glasses") as MicProbeSource
|
|
61
|
+
return {
|
|
62
|
+
durationMs,
|
|
63
|
+
a2dp,
|
|
64
|
+
toneLevel: Number.isFinite(toneLevel) && toneLevel > 0 ? Math.min(1, toneLevel) : 0.2,
|
|
65
|
+
source,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type MicProbeSample = {
|
|
70
|
+
/** Seconds since the probe started. */
|
|
71
|
+
t: number
|
|
72
|
+
/** Mean absolute sample value over the last second (16-bit scale). */
|
|
73
|
+
meanAbs: number
|
|
74
|
+
/** Largest absolute sample over the last second. */
|
|
75
|
+
peak: number
|
|
76
|
+
/** PCM frames received in the last second. */
|
|
77
|
+
frames: number
|
|
78
|
+
/** Microphone source reported by the SDK for the last frame. */
|
|
79
|
+
source: string
|
|
80
|
+
/** Frames dropped because the SDK reported a non-glasses source. */
|
|
81
|
+
nonGlasses: number
|
|
82
|
+
a2dp: MicProbeA2dpMode
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
type Listener = (sample: MicProbeSample) => void
|
|
86
|
+
|
|
87
|
+
const GLASSES = "glasses"
|
|
88
|
+
const A2DP_RATE = 16000
|
|
89
|
+
const A2DP_CHUNK_MS = 60
|
|
90
|
+
const TONE_HZ = 440
|
|
91
|
+
|
|
92
|
+
// Same meter the call uplink logs with, so probe and call numbers are comparable.
|
|
93
|
+
export {pcmDataView, summarizePcm16} from "../utils/pcm16"
|
|
94
|
+
|
|
95
|
+
/** `chunkMs` of 16 kHz mono PCM16, either digital silence or a sine at `level` full scale. */
|
|
96
|
+
export function makeA2dpChunk(mode: Exclude<MicProbeA2dpMode, "none">, chunkMs: number, phase: number, level = 0.2) {
|
|
97
|
+
const samples = Math.round((A2DP_RATE * chunkMs) / 1000)
|
|
98
|
+
const bytes = new Uint8Array(samples * 2)
|
|
99
|
+
const view = new DataView(bytes.buffer)
|
|
100
|
+
const amp = Math.max(0, Math.min(1, level)) * 32767
|
|
101
|
+
const step = (2 * Math.PI * TONE_HZ) / A2DP_RATE
|
|
102
|
+
for (let i = 0; i < samples; i++) {
|
|
103
|
+
const v = mode === "tone" ? Math.round(Math.sin(phase + i * step) * amp) : 0
|
|
104
|
+
view.setInt16(i * 2, v, true)
|
|
105
|
+
}
|
|
106
|
+
return {bytes, phase: (phase + samples * step) % (2 * Math.PI)}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function bytesToBase64(bytes: Uint8Array): string {
|
|
110
|
+
let binary = ""
|
|
111
|
+
const step = 0x8000
|
|
112
|
+
for (let i = 0; i < bytes.length; i += step) {
|
|
113
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + step))
|
|
114
|
+
}
|
|
115
|
+
return btoa(binary)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
class GlassesMicProbe {
|
|
119
|
+
private running = false
|
|
120
|
+
private stopping: Promise<void> | null = null
|
|
121
|
+
/** Bumped on every start/stop so an await that loses the race cannot re-arm the probe. */
|
|
122
|
+
private generation = 0
|
|
123
|
+
private options: MicProbeOptions | null = null
|
|
124
|
+
private startedAt = 0
|
|
125
|
+
private micSub: {remove: () => void} | null = null
|
|
126
|
+
private tick: number | null = null
|
|
127
|
+
private stopTimer: number | null = null
|
|
128
|
+
private a2dpTimer: number | null = null
|
|
129
|
+
private a2dpStreamId: string | null = null
|
|
130
|
+
private a2dpPhase = 0
|
|
131
|
+
private window: unknown[] = []
|
|
132
|
+
private lastSource = ""
|
|
133
|
+
private nonGlasses = 0
|
|
134
|
+
/** `preferred_mic` before a `source=phone` control run changed it. */
|
|
135
|
+
private savedPreferredMic: string | null = null
|
|
136
|
+
private listeners = new Set<Listener>()
|
|
137
|
+
private lastSample: MicProbeSample | null = null
|
|
138
|
+
|
|
139
|
+
isRunning(): boolean {
|
|
140
|
+
return this.running
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
last(): MicProbeSample | null {
|
|
144
|
+
return this.lastSample
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
subscribe(listener: Listener): () => void {
|
|
148
|
+
this.listeners.add(listener)
|
|
149
|
+
return () => this.listeners.delete(listener)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async start(options: MicProbeOptions): Promise<void> {
|
|
153
|
+
// A stop still unpinning the mic must finish before we pin again, or its unpin lands on us.
|
|
154
|
+
if (this.running) await this.stop()
|
|
155
|
+
else if (this.stopping) await this.stopping
|
|
156
|
+
const generation = ++this.generation
|
|
157
|
+
this.running = true
|
|
158
|
+
this.options = options
|
|
159
|
+
this.startedAt = Date.now()
|
|
160
|
+
this.window = []
|
|
161
|
+
this.nonGlasses = 0
|
|
162
|
+
this.lastSource = ""
|
|
163
|
+
this.lastSample = null
|
|
164
|
+
const source = options.source ?? GLASSES
|
|
165
|
+
console.log("[MIC_PROBE] start", options)
|
|
166
|
+
|
|
167
|
+
if (source === GLASSES) {
|
|
168
|
+
// Same order as AcsMeetingService.startGlassesMicUplink: pin first so the first frame the
|
|
169
|
+
// requirement produces is already from the glasses and the phone mic is never opened.
|
|
170
|
+
await Promise.resolve(BluetoothSdk.setMicSourcePin?.(GLASSES)).catch((error) => {
|
|
171
|
+
console.warn("[MIC_PROBE] pin failed", error)
|
|
172
|
+
})
|
|
173
|
+
} else {
|
|
174
|
+
// Control run: no pin (only the glasses can be pinned); steer the ranking with the
|
|
175
|
+
// user preference and put it back on stop.
|
|
176
|
+
const settings = useSettingsStore.getState()
|
|
177
|
+
this.savedPreferredMic = settings.getSetting(SETTINGS.preferred_mic.key) ?? "auto"
|
|
178
|
+
await settings.setSetting(SETTINGS.preferred_mic.key, source, false)
|
|
179
|
+
}
|
|
180
|
+
if (this.generation !== generation) return
|
|
181
|
+
micStateCoordinator.setCallRequirement(true)
|
|
182
|
+
this.micSub = BluetoothSdk.addListener("mic_pcm", (event: {pcm?: unknown; source?: string}) => {
|
|
183
|
+
this.lastSource = event.source ?? ""
|
|
184
|
+
if (event.source !== source) {
|
|
185
|
+
this.nonGlasses += 1
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
if (event.pcm) this.window.push(event.pcm)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
if (options.a2dp !== "none") await this.startA2dp(options.a2dp, options.toneLevel ?? 0.2)
|
|
192
|
+
if (this.generation !== generation) return
|
|
193
|
+
|
|
194
|
+
this.tick = BgTimer.setInterval(() => this.report(), 1000)
|
|
195
|
+
this.stopTimer = BgTimer.setTimeout(() => void this.stop(), options.durationMs)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async stop(): Promise<void> {
|
|
199
|
+
this.generation += 1
|
|
200
|
+
if (!this.running) return this.stopping ?? undefined
|
|
201
|
+
this.running = false
|
|
202
|
+
if (this.tick !== null) BgTimer.clearInterval(this.tick)
|
|
203
|
+
if (this.stopTimer !== null) BgTimer.clearTimeout(this.stopTimer)
|
|
204
|
+
this.tick = null
|
|
205
|
+
this.stopTimer = null
|
|
206
|
+
this.report()
|
|
207
|
+
this.micSub?.remove()
|
|
208
|
+
this.micSub = null
|
|
209
|
+
micStateCoordinator.setCallRequirement(false)
|
|
210
|
+
this.stopping = (async () => {
|
|
211
|
+
if ((this.options?.source ?? GLASSES) === GLASSES) {
|
|
212
|
+
await Promise.resolve(BluetoothSdk.setMicSourcePin?.(null)).catch((error) => {
|
|
213
|
+
console.warn("[MIC_PROBE] unpin failed", error)
|
|
214
|
+
})
|
|
215
|
+
} else if (this.savedPreferredMic !== null) {
|
|
216
|
+
const restore = this.savedPreferredMic
|
|
217
|
+
this.savedPreferredMic = null
|
|
218
|
+
await useSettingsStore.getState().setSetting(SETTINGS.preferred_mic.key, restore, false)
|
|
219
|
+
}
|
|
220
|
+
await this.stopA2dp()
|
|
221
|
+
console.log("[MIC_PROBE] stop")
|
|
222
|
+
})().finally(() => {
|
|
223
|
+
this.stopping = null
|
|
224
|
+
})
|
|
225
|
+
return this.stopping
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private report(): void {
|
|
229
|
+
const frames = this.window
|
|
230
|
+
this.window = []
|
|
231
|
+
let stats = {meanAbs: 0, peak: 0}
|
|
232
|
+
try {
|
|
233
|
+
stats = summarizePcm16(frames)
|
|
234
|
+
} catch (error) {
|
|
235
|
+
const first = frames[0]
|
|
236
|
+
console.warn("[MIC_PROBE] pcm summarize failed", {
|
|
237
|
+
frames: frames.length,
|
|
238
|
+
type: first == null ? "empty" : Object.prototype.toString.call(first),
|
|
239
|
+
byteLength: (first as {byteLength?: number} | undefined)?.byteLength,
|
|
240
|
+
error,
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
const sample: MicProbeSample = {
|
|
244
|
+
t: Math.round((Date.now() - this.startedAt) / 1000),
|
|
245
|
+
meanAbs: stats.meanAbs,
|
|
246
|
+
peak: stats.peak,
|
|
247
|
+
frames: frames.length,
|
|
248
|
+
source: this.lastSource,
|
|
249
|
+
nonGlasses: this.nonGlasses,
|
|
250
|
+
a2dp: this.options?.a2dp ?? "none",
|
|
251
|
+
}
|
|
252
|
+
this.lastSample = sample
|
|
253
|
+
console.log(
|
|
254
|
+
`[MIC_PROBE] t=${sample.t} meanAbs=${sample.meanAbs} peak=${sample.peak} frames=${frames.length} ` +
|
|
255
|
+
`source=${sample.source || "-"} nonGlasses=${sample.nonGlasses} a2dp=${sample.a2dp}`,
|
|
256
|
+
)
|
|
257
|
+
for (const listener of this.listeners) listener(sample)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private async startA2dp(mode: Exclude<MicProbeA2dpMode, "none">, level: number): Promise<void> {
|
|
261
|
+
const streamId = `mic-probe-${Date.now()}`
|
|
262
|
+
await audioPlaybackService.openStream({
|
|
263
|
+
streamId,
|
|
264
|
+
appId: "mic-probe",
|
|
265
|
+
sampleRate: A2DP_RATE,
|
|
266
|
+
channels: 1,
|
|
267
|
+
stopOtherAudio: true,
|
|
268
|
+
jitterMs: 120,
|
|
269
|
+
onEnded: () => {
|
|
270
|
+
if (this.a2dpStreamId === streamId) this.a2dpStreamId = null
|
|
271
|
+
},
|
|
272
|
+
})
|
|
273
|
+
this.a2dpStreamId = streamId
|
|
274
|
+
this.a2dpPhase = 0
|
|
275
|
+
let inFlight = false
|
|
276
|
+
this.a2dpTimer = BgTimer.setInterval(() => {
|
|
277
|
+
if (inFlight || this.a2dpStreamId !== streamId) return
|
|
278
|
+
inFlight = true
|
|
279
|
+
const chunk = makeA2dpChunk(mode, A2DP_CHUNK_MS, this.a2dpPhase, level)
|
|
280
|
+
this.a2dpPhase = chunk.phase
|
|
281
|
+
audioPlaybackService
|
|
282
|
+
.writeStreamChunk(streamId, bytesToBase64(chunk.bytes))
|
|
283
|
+
.catch((error) => console.warn("[MIC_PROBE] a2dp write failed", error))
|
|
284
|
+
.finally(() => {
|
|
285
|
+
inFlight = false
|
|
286
|
+
})
|
|
287
|
+
}, A2DP_CHUNK_MS)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private async stopA2dp(): Promise<void> {
|
|
291
|
+
if (this.a2dpTimer !== null) BgTimer.clearInterval(this.a2dpTimer)
|
|
292
|
+
this.a2dpTimer = null
|
|
293
|
+
const streamId = this.a2dpStreamId
|
|
294
|
+
this.a2dpStreamId = null
|
|
295
|
+
if (streamId) await audioPlaybackService.abortStream(streamId).catch(() => undefined)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const glassesMicProbe = new GlassesMicProbe()
|
|
300
|
+
export default glassesMicProbe
|