@mentra/engine 3.2.0-dev.116 → 3.2.0-dev.120
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/engine.d.ts.map +1 -1
- package/build/engine.js +7 -2
- package/build/engine.js.map +1 -1
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/runtime/streamConfig.d.ts +7 -0
- package/build/runtime/streamConfig.d.ts.map +1 -1
- package/build/runtime/streamConfig.js +7 -0
- package/build/runtime/streamConfig.js.map +1 -1
- package/build/services/AcsMeetingService.d.ts +154 -0
- package/build/services/AcsMeetingService.d.ts.map +1 -0
- package/build/services/AcsMeetingService.js +477 -0
- package/build/services/AcsMeetingService.js.map +1 -0
- package/build/services/DeviceEventRouter.d.ts.map +1 -1
- package/build/services/DeviceEventRouter.js +12 -3
- package/build/services/DeviceEventRouter.js.map +1 -1
- package/build/services/GlassesSettingsSync.d.ts.map +1 -1
- package/build/services/GlassesSettingsSync.js +11 -1
- package/build/services/GlassesSettingsSync.js.map +1 -1
- package/build/services/GlassesStatusProjection.d.ts +12 -0
- package/build/services/GlassesStatusProjection.d.ts.map +1 -1
- package/build/services/GlassesStatusProjection.js +23 -0
- package/build/services/GlassesStatusProjection.js.map +1 -1
- package/build/services/LocalMiniappRuntime.d.ts +6 -0
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +130 -1
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/MentraJSRouter.d.ts.map +1 -1
- package/build/services/MentraJSRouter.js +8 -1
- package/build/services/MentraJSRouter.js.map +1 -1
- package/build/services/PhoneCameraFovCoordinator.d.ts +18 -0
- package/build/services/PhoneCameraFovCoordinator.d.ts.map +1 -1
- package/build/services/PhoneCameraFovCoordinator.js +36 -0
- package/build/services/PhoneCameraFovCoordinator.js.map +1 -1
- package/build/services/PhoneStreamCoordinator.d.ts +67 -1
- package/build/services/PhoneStreamCoordinator.d.ts.map +1 -1
- package/build/services/PhoneStreamCoordinator.js +204 -15
- package/build/services/PhoneStreamCoordinator.js.map +1 -1
- package/build/services/StreamLifecycleController.d.ts +23 -5
- package/build/services/StreamLifecycleController.d.ts.map +1 -1
- package/build/services/StreamLifecycleController.js +31 -10
- package/build/services/StreamLifecycleController.js.map +1 -1
- package/build/services/acsAudioSource.d.ts +24 -0
- package/build/services/acsAudioSource.d.ts.map +1 -0
- package/build/services/acsAudioSource.js +37 -0
- package/build/services/acsAudioSource.js.map +1 -0
- package/build/utils/devMiniappLaunch.d.ts.map +1 -1
- package/build/utils/devMiniappLaunch.js +4 -0
- package/build/utils/devMiniappLaunch.js.map +1 -1
- package/package.json +7 -6
- package/src/engine.ts +10 -2
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/runtime/streamConfig.ts +13 -0
- package/src/services/AcsMeetingService.ts +570 -0
- package/src/services/DeviceEventRouter.ts +12 -3
- package/src/services/GlassesSettingsSync.ts +13 -3
- package/src/services/GlassesStatusProjection.ts +28 -0
- package/src/services/LocalMiniappRuntime.ts +142 -1
- package/src/services/MentraJSRouter.ts +7 -1
- package/src/services/PhoneCameraFovCoordinator.ts +34 -0
- package/src/services/PhoneStreamCoordinator.ts +232 -19
- package/src/services/StreamLifecycleController.ts +42 -12
- package/src/services/acsAudioSource.ts +53 -0
- package/src/utils/devMiniappLaunch.ts +4 -0
|
@@ -26,6 +26,18 @@
|
|
|
26
26
|
* streams. We use a `phone-` prefix so they're trivially distinguishable from
|
|
27
27
|
* cloud-minted IDs in logs and from cloud-SDK app streams that flow through
|
|
28
28
|
* the legacy path.
|
|
29
|
+
*
|
|
30
|
+
* BLE link loss is a SUSPENDED state, not a failure:
|
|
31
|
+
* The glasses publisher keeps pushing over Wi-Fi when the phone's Bluetooth
|
|
32
|
+
* link drops; only our keep-alives stop reaching it. While the store says
|
|
33
|
+
* the glasses are disconnected we pause the heartbeat (so we don't count
|
|
34
|
+
* misses we caused ourselves), fan out `suspended`, and give the link
|
|
35
|
+
* `glassesGraceMs` to come back. On reconnect we resume, heartbeat at once,
|
|
36
|
+
* and fan out `resumed` with the SAME streamId — no re-provision. The grace
|
|
37
|
+
* ends early when Cloudflare reports the publisher gone for two probes in
|
|
38
|
+
* a row (glasses powered off, not a BLE hiccup). If the grace expires the
|
|
39
|
+
* stream is torn down with reason `glasses_disconnected`, and the BLE stop
|
|
40
|
+
* the glasses never received is sent on the next reconnect.
|
|
29
41
|
*/
|
|
30
42
|
import BluetoothSdk from "@mentra/bluetooth-sdk/internal";
|
|
31
43
|
import { isGlassesConnected } from "./GlassesReadiness";
|
|
@@ -49,6 +61,23 @@ const DEFAULT_TIMINGS = {
|
|
|
49
61
|
hlsReadinessInitialDelayMs: 5_000,
|
|
50
62
|
hlsReadinessPollMs: 2_000,
|
|
51
63
|
hlsReadinessMaxAttempts: 30,
|
|
64
|
+
// How long a stream survives a BLE link drop before we give up on it. Must
|
|
65
|
+
// stay under the glasses publisher's own 60s no-keep-alive watchdog with
|
|
66
|
+
// margin for the resume heartbeat's round trip.
|
|
67
|
+
glassesGraceMs: 45_000,
|
|
68
|
+
// Consecutive Cloudflare "publisher disconnected" probes while suspended
|
|
69
|
+
// before we conclude the glasses are off (not just out of BLE range).
|
|
70
|
+
suspendedPublisherGoneProbes: 2,
|
|
71
|
+
};
|
|
72
|
+
const storeLinkSource = {
|
|
73
|
+
isConnected: () => isGlassesConnected(useGlassesStore.getState().connection),
|
|
74
|
+
subscribe: (listener) => useGlassesStore.subscribe((s) => isGlassesConnected(s.connection), listener),
|
|
75
|
+
};
|
|
76
|
+
/** Reasons carried on coordinator-sourced `stream_status` fanouts for link events. */
|
|
77
|
+
export const LINK_STATUS = {
|
|
78
|
+
suspended: "suspended",
|
|
79
|
+
resumed: "resumed",
|
|
80
|
+
reason: "glasses_disconnected",
|
|
52
81
|
};
|
|
53
82
|
// Console-backed minimal logger; replaces pino on the phone.
|
|
54
83
|
const consoleLogger = {
|
|
@@ -78,22 +107,22 @@ export class StreamConflictError extends Error {
|
|
|
78
107
|
this.name = "StreamConflictError";
|
|
79
108
|
}
|
|
80
109
|
}
|
|
81
|
-
/**
|
|
82
|
-
* Fail fast if glasses aren't connected — BEFORE provisioning. Without this a
|
|
83
|
-
* managed start would create a provider live input, fail the BLE command, and
|
|
84
|
-
* tear the input down again: a slow, billable no-op with a confusing error.
|
|
85
|
-
*/
|
|
86
|
-
function assertGlassesConnected() {
|
|
87
|
-
if (!isGlassesConnected(useGlassesStore.getState().connection)) {
|
|
88
|
-
throw new StreamConflictError("GLASSES_NOT_CONNECTED", "Glasses are not connected", "command", "ble");
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
110
|
export class PhoneStreamCoordinator {
|
|
92
111
|
current = null;
|
|
93
112
|
lifecycle = null;
|
|
94
113
|
statusSubscriber = null;
|
|
95
114
|
idCounter = 0;
|
|
96
115
|
timings;
|
|
116
|
+
linkSource;
|
|
117
|
+
unsubscribeLink = null;
|
|
118
|
+
suspended = null;
|
|
119
|
+
/**
|
|
120
|
+
* A stream was torn down while the BLE link was down, so the glasses never
|
|
121
|
+
* got `stopStream`. Sent on the next reconnect (if no new stream has claimed
|
|
122
|
+
* the slot) so a publisher that outlived its input does not keep pushing
|
|
123
|
+
* until its own watchdog fires.
|
|
124
|
+
*/
|
|
125
|
+
pendingBleStop = null;
|
|
97
126
|
/**
|
|
98
127
|
* Serializes state transitions (start, stop, teardown). Without it, a
|
|
99
128
|
* second `start*` racing with the first can pass the `this.current === null`
|
|
@@ -107,8 +136,23 @@ export class PhoneStreamCoordinator {
|
|
|
107
136
|
lastFanoutSignature = null;
|
|
108
137
|
/** Send full resolvedConfig only once per stream session. */
|
|
109
138
|
resolvedConfigForwarded = false;
|
|
110
|
-
constructor(timings = {}) {
|
|
139
|
+
constructor(timings = {}, deps = {}) {
|
|
111
140
|
this.timings = { ...DEFAULT_TIMINGS, ...timings };
|
|
141
|
+
this.linkSource = deps.linkSource ?? storeLinkSource;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Fail fast if glasses aren't connected — BEFORE provisioning. Without this a
|
|
145
|
+
* managed start would create a provider live input, fail the BLE command, and
|
|
146
|
+
* tear the input down again: a slow, billable no-op with a confusing error.
|
|
147
|
+
*/
|
|
148
|
+
assertGlassesConnected() {
|
|
149
|
+
if (!this.linkSource.isConnected()) {
|
|
150
|
+
throw new StreamConflictError("GLASSES_NOT_CONNECTED", "Glasses are not connected", "command", "ble");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** True while the active stream is parked on a dropped BLE link. */
|
|
154
|
+
isSuspended() {
|
|
155
|
+
return this.suspended !== null;
|
|
112
156
|
}
|
|
113
157
|
/**
|
|
114
158
|
* Run `work` under the transition lock. Each call awaits the previous
|
|
@@ -146,7 +190,11 @@ export class PhoneStreamCoordinator {
|
|
|
146
190
|
/** Report-safe stream ownership snapshot for incident diagnostics. */
|
|
147
191
|
getDiagnosticSnapshot() {
|
|
148
192
|
if (!this.current)
|
|
149
|
-
return { active: false };
|
|
193
|
+
return { active: false, pendingBleStop: this.pendingBleStop?.streamId ?? null };
|
|
194
|
+
const link = {
|
|
195
|
+
suspended: this.suspended !== null,
|
|
196
|
+
...(this.suspended ? { suspendedForMs: Date.now() - this.suspended.since } : {}),
|
|
197
|
+
};
|
|
150
198
|
return this.current.kind === "managed"
|
|
151
199
|
? {
|
|
152
200
|
active: true,
|
|
@@ -155,12 +203,14 @@ export class PhoneStreamCoordinator {
|
|
|
155
203
|
subscribers: [...this.current.subscribers].sort(),
|
|
156
204
|
mode: this.current.mode,
|
|
157
205
|
playbackReady: this.current.hlsReady,
|
|
206
|
+
...link,
|
|
158
207
|
}
|
|
159
208
|
: {
|
|
160
209
|
active: true,
|
|
161
210
|
kind: this.current.kind,
|
|
162
211
|
streamId: this.current.streamId,
|
|
163
212
|
ownerPackageName: this.current.packageName,
|
|
213
|
+
...link,
|
|
164
214
|
};
|
|
165
215
|
}
|
|
166
216
|
async startUnmanaged(packageName, opts) {
|
|
@@ -169,7 +219,7 @@ export class PhoneStreamCoordinator {
|
|
|
169
219
|
if (!opts.streamUrl || typeof opts.streamUrl !== "string") {
|
|
170
220
|
throw new StreamConflictError("STREAM_URL_REQUIRED", "streamUrl is required");
|
|
171
221
|
}
|
|
172
|
-
assertGlassesConnected();
|
|
222
|
+
this.assertGlassesConnected();
|
|
173
223
|
return this.runExclusive(async () => {
|
|
174
224
|
if (this.current) {
|
|
175
225
|
throw new StreamConflictError("STREAM_ALREADY_ACTIVE", `A ${this.current.kind} stream is already active. Stop it before starting a new one.`);
|
|
@@ -195,6 +245,7 @@ export class PhoneStreamCoordinator {
|
|
|
195
245
|
...(opts.video !== undefined ? { video: opts.video } : {}),
|
|
196
246
|
...(opts.audio !== undefined ? { audio: opts.audio } : {}),
|
|
197
247
|
...(opts.authToken ? { authToken: opts.authToken } : {}),
|
|
248
|
+
...(typeof opts.captureAudio === "boolean" ? { captureAudio: opts.captureAudio } : {}),
|
|
198
249
|
});
|
|
199
250
|
const result = publisherStartResult(streamId, event);
|
|
200
251
|
this.startLifecycle(streamId);
|
|
@@ -219,7 +270,7 @@ export class PhoneStreamCoordinator {
|
|
|
219
270
|
// Two-phase: the entry-claim runs under the transition lock; the wait for
|
|
220
271
|
// HLS readiness happens AFTER the lock releases so a long warm-up doesn't
|
|
221
272
|
// block subsequent start/stop transitions on this coordinator.
|
|
222
|
-
assertGlassesConnected();
|
|
273
|
+
this.assertGlassesConnected();
|
|
223
274
|
const decision = await this.runExclusive(async () => {
|
|
224
275
|
if (this.current && this.current.kind === "unmanaged") {
|
|
225
276
|
throw new StreamConflictError("STREAM_ALREADY_ACTIVE", "An unmanaged stream is already active. Stop it before starting a managed stream.");
|
|
@@ -279,6 +330,7 @@ export class PhoneStreamCoordinator {
|
|
|
279
330
|
// See startUnmanaged: the native bridge rejects explicit `undefined`.
|
|
280
331
|
...(opts.video !== undefined ? { video: opts.video } : {}),
|
|
281
332
|
...(opts.audio !== undefined ? { audio: opts.audio } : {}),
|
|
333
|
+
...(typeof opts.captureAudio === "boolean" ? { captureAudio: opts.captureAudio } : {}),
|
|
282
334
|
});
|
|
283
335
|
entry.publisherStart = publisherStartResult(streamId, event);
|
|
284
336
|
console.info("[STREAM_STARTUP]", {
|
|
@@ -396,13 +448,120 @@ export class PhoneStreamCoordinator {
|
|
|
396
448
|
this.lifecycle?.handleAck(event.ackId);
|
|
397
449
|
}
|
|
398
450
|
// ===========================================================================
|
|
451
|
+
// BLE link suspension
|
|
452
|
+
// ===========================================================================
|
|
453
|
+
attachLink() {
|
|
454
|
+
if (this.unsubscribeLink)
|
|
455
|
+
return;
|
|
456
|
+
this.unsubscribeLink = this.linkSource.subscribe((connected) => this.handleLinkChange(connected));
|
|
457
|
+
}
|
|
458
|
+
detachLinkIfIdle() {
|
|
459
|
+
if (this.current || this.pendingBleStop || !this.unsubscribeLink)
|
|
460
|
+
return;
|
|
461
|
+
this.unsubscribeLink();
|
|
462
|
+
this.unsubscribeLink = null;
|
|
463
|
+
}
|
|
464
|
+
handleLinkChange(connected) {
|
|
465
|
+
if (connected) {
|
|
466
|
+
if (this.current && this.suspended) {
|
|
467
|
+
this.resumeLocked();
|
|
468
|
+
}
|
|
469
|
+
else if (!this.current && this.pendingBleStop) {
|
|
470
|
+
this.flushPendingBleStop();
|
|
471
|
+
}
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (this.current && !this.suspended)
|
|
475
|
+
this.suspend();
|
|
476
|
+
}
|
|
477
|
+
suspend() {
|
|
478
|
+
const entry = this.current;
|
|
479
|
+
if (!entry)
|
|
480
|
+
return;
|
|
481
|
+
const since = Date.now();
|
|
482
|
+
// Pause the heartbeat: misses now would be our own fault, not the
|
|
483
|
+
// publisher's, and counting them would reap a stream that is still live.
|
|
484
|
+
this.lifecycle?.setActive(false);
|
|
485
|
+
const graceTimer = setTimeout(() => this.onGraceExpired(entry.streamId), this.timings.glassesGraceMs);
|
|
486
|
+
this.suspended = { since, graceTimer, publisherGoneProbes: 0 };
|
|
487
|
+
console.warn("[STREAM] BLE link lost; stream suspended", {
|
|
488
|
+
streamId: entry.streamId,
|
|
489
|
+
graceMs: this.timings.glassesGraceMs,
|
|
490
|
+
});
|
|
491
|
+
this.fanout({
|
|
492
|
+
streamId: entry.streamId,
|
|
493
|
+
source: "coordinator",
|
|
494
|
+
status: LINK_STATUS.suspended,
|
|
495
|
+
data: { reason: LINK_STATUS.reason, graceMs: this.timings.glassesGraceMs, since },
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
resumeLocked() {
|
|
499
|
+
const entry = this.current;
|
|
500
|
+
const suspended = this.suspended;
|
|
501
|
+
if (!entry || !suspended)
|
|
502
|
+
return;
|
|
503
|
+
clearTimeout(suspended.graceTimer);
|
|
504
|
+
this.suspended = null;
|
|
505
|
+
const suspendedMs = Date.now() - suspended.since;
|
|
506
|
+
console.info("[STREAM] BLE link back; stream resumed", { streamId: entry.streamId, suspendedMs });
|
|
507
|
+
// Resume and heartbeat immediately: the glasses watchdog has been running
|
|
508
|
+
// the whole time, so the first keep-alive must not wait another interval.
|
|
509
|
+
this.lifecycle?.setActive(true);
|
|
510
|
+
this.lifecycle?.tickNow();
|
|
511
|
+
// A resumed session is a fresh status baseline for subscribers.
|
|
512
|
+
this.lastFanoutSignature = null;
|
|
513
|
+
this.fanout({
|
|
514
|
+
streamId: entry.streamId,
|
|
515
|
+
source: "coordinator",
|
|
516
|
+
status: LINK_STATUS.resumed,
|
|
517
|
+
data: { reason: LINK_STATUS.reason, suspendedMs },
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
onGraceExpired(streamId) {
|
|
521
|
+
if (this.current?.streamId !== streamId || !this.suspended)
|
|
522
|
+
return;
|
|
523
|
+
this.failSuspended(streamId, "glasses_disconnected", { publisherGone: false });
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* End a suspended stream. `publisherGone` distinguishes "glasses are off and
|
|
527
|
+
* Cloudflare confirms nothing is publishing" from "grace ran out with the
|
|
528
|
+
* publisher possibly still alive" — miniapps word the two differently.
|
|
529
|
+
*/
|
|
530
|
+
failSuspended(streamId, reason, detail) {
|
|
531
|
+
this.fanout({
|
|
532
|
+
streamId,
|
|
533
|
+
source: "coordinator",
|
|
534
|
+
status: "error",
|
|
535
|
+
data: { reason: LINK_STATUS.reason, teardownReason: reason, ...detail },
|
|
536
|
+
});
|
|
537
|
+
void this.runExclusive(async () => {
|
|
538
|
+
if (this.current?.streamId !== streamId)
|
|
539
|
+
return;
|
|
540
|
+
await this.teardownLocked(reason);
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
flushPendingBleStop() {
|
|
544
|
+
const pending = this.pendingBleStop;
|
|
545
|
+
if (!pending)
|
|
546
|
+
return;
|
|
547
|
+
this.pendingBleStop = null;
|
|
548
|
+
console.info("[STREAM] BLE link back; sending deferred stopStream", pending);
|
|
549
|
+
void BluetoothSdk.stopStream()
|
|
550
|
+
.catch((err) => console.warn("[STREAM] deferred stopStream failed:", err))
|
|
551
|
+
.finally(() => this.detachLinkIfIdle());
|
|
552
|
+
}
|
|
553
|
+
// ===========================================================================
|
|
399
554
|
// Internal
|
|
400
555
|
// ===========================================================================
|
|
401
556
|
mintId(prefix) {
|
|
402
557
|
this.idCounter += 1;
|
|
403
558
|
return `phone-${prefix}-${Date.now().toString(36)}-${this.idCounter}`;
|
|
404
559
|
}
|
|
560
|
+
/** BLE keep-alives. StreamLifecycleController uses BgTimer so they survive MentraOS backgrounding. */
|
|
405
561
|
startLifecycle(streamId) {
|
|
562
|
+
// A new stream supersedes any deferred stop for the previous one.
|
|
563
|
+
this.pendingBleStop = null;
|
|
564
|
+
this.attachLink();
|
|
406
565
|
this.lifecycle?.dispose();
|
|
407
566
|
const ctrl = new StreamLifecycleController({
|
|
408
567
|
logger: consoleLogger,
|
|
@@ -475,6 +634,21 @@ export class PhoneStreamCoordinator {
|
|
|
475
634
|
status: status.isConnected ? "connected" : "disconnected",
|
|
476
635
|
data: status,
|
|
477
636
|
});
|
|
637
|
+
// While the BLE link is down, Cloudflare is the only witness to the
|
|
638
|
+
// publisher. Two consecutive "nobody is publishing" probes mean the
|
|
639
|
+
// glasses are off, not merely out of Bluetooth range — stop waiting.
|
|
640
|
+
if (this.suspended) {
|
|
641
|
+
if (status.isConnected) {
|
|
642
|
+
this.suspended.publisherGoneProbes = 0;
|
|
643
|
+
}
|
|
644
|
+
else {
|
|
645
|
+
this.suspended.publisherGoneProbes += 1;
|
|
646
|
+
if (this.suspended.publisherGoneProbes >= this.timings.suspendedPublisherGoneProbes) {
|
|
647
|
+
this.failSuspended(entry.streamId, "glasses_disconnected_publisher_gone", { publisherGone: true });
|
|
648
|
+
keepPolling = false;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
478
652
|
// webrtc mode readiness: first "connected" means WHEP playback is
|
|
479
653
|
// available (WebRTC playback follows the ingest directly; there is no
|
|
480
654
|
// manifest to probe).
|
|
@@ -652,11 +826,25 @@ export class PhoneStreamCoordinator {
|
|
|
652
826
|
const sendBleStop = options.sendBleStop !== false;
|
|
653
827
|
this.lastFanoutSignature = null;
|
|
654
828
|
this.resolvedConfigForwarded = false;
|
|
829
|
+
if (this.suspended) {
|
|
830
|
+
clearTimeout(this.suspended.graceTimer);
|
|
831
|
+
this.suspended = null;
|
|
832
|
+
}
|
|
655
833
|
// Dispose the lifecycle controller immediately so it doesn't fire one
|
|
656
834
|
// more keep-alive against a stream we're tearing down. The transition
|
|
657
835
|
// lock guarantees no new lifecycle is started concurrently.
|
|
658
836
|
this.lifecycle?.dispose();
|
|
659
837
|
this.lifecycle = null;
|
|
838
|
+
// With the link down a BLE write can only fail (and hold the transition
|
|
839
|
+
// lock for the native timeout). Defer it to the next reconnect instead.
|
|
840
|
+
const linkUp = this.linkSource.isConnected();
|
|
841
|
+
if (sendBleStop && !linkUp) {
|
|
842
|
+
this.pendingBleStop = { streamId: entry.streamId };
|
|
843
|
+
console.warn("[STREAM] BLE link down during teardown; stopStream deferred", {
|
|
844
|
+
streamId: entry.streamId,
|
|
845
|
+
reason,
|
|
846
|
+
});
|
|
847
|
+
}
|
|
660
848
|
if (entry.kind === "managed") {
|
|
661
849
|
if (entry.cloudflareTimer)
|
|
662
850
|
clearTimeout(entry.cloudflareTimer);
|
|
@@ -670,7 +858,7 @@ export class PhoneStreamCoordinator {
|
|
|
670
858
|
entry.hlsReadyRejecters = [];
|
|
671
859
|
}
|
|
672
860
|
try {
|
|
673
|
-
if (sendBleStop) {
|
|
861
|
+
if (sendBleStop && linkUp) {
|
|
674
862
|
await BluetoothSdk.stopStream();
|
|
675
863
|
}
|
|
676
864
|
}
|
|
@@ -684,6 +872,7 @@ export class PhoneStreamCoordinator {
|
|
|
684
872
|
// serializes us, so this should always be true).
|
|
685
873
|
if (this.current === entry)
|
|
686
874
|
this.current = null;
|
|
875
|
+
this.detachLinkIfIdle();
|
|
687
876
|
if (entry.kind === "managed") {
|
|
688
877
|
// Start remote cleanup only after the publisher has stopped, but do not
|
|
689
878
|
// hold the local transition lock on an unbounded network request. The
|