@mentra/engine 3.2.0-dev.200 → 3.2.0-dev.208
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/services/DeviceEventRouter.d.ts.map +1 -1
- package/build/services/DeviceEventRouter.js +3 -7
- package/build/services/DeviceEventRouter.js.map +1 -1
- package/build/services/PhoneStreamCoordinator.d.ts +7 -20
- package/build/services/PhoneStreamCoordinator.d.ts.map +1 -1
- package/build/services/PhoneStreamCoordinator.js +16 -99
- package/build/services/PhoneStreamCoordinator.js.map +1 -1
- package/build/services/asg/galleryNotices.d.ts +1 -1
- package/build/services/asg/galleryNotices.d.ts.map +1 -1
- package/build/services/asg/galleryNotices.js.map +1 -1
- package/build/services/asg/gallerySyncService.d.ts +14 -0
- package/build/services/asg/gallerySyncService.d.ts.map +1 -1
- package/build/services/asg/gallerySyncService.js +232 -96
- package/build/services/asg/gallerySyncService.js.map +1 -1
- package/build/services/slimStreamStatus.d.ts.map +1 -1
- package/build/services/slimStreamStatus.js +16 -0
- package/build/services/slimStreamStatus.js.map +1 -1
- package/package.json +7 -7
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/services/DeviceEventRouter.ts +5 -9
- package/src/services/PhoneStreamCoordinator.ts +23 -128
- package/src/services/asg/galleryNotices.ts +1 -0
- package/src/services/asg/gallerySyncService.ts +260 -112
- package/src/services/slimStreamStatus.ts +10 -1
- package/build/services/StreamLifecycleController.d.ts +0 -85
- package/build/services/StreamLifecycleController.d.ts.map +0 -1
- package/build/services/StreamLifecycleController.js +0 -173
- package/build/services/StreamLifecycleController.js.map +0 -1
- package/src/services/StreamLifecycleController.ts +0 -243
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slimStreamStatus.d.ts","sourceRoot":"","sources":["../../src/services/slimStreamStatus.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,gCAAgC,CAAA;AAErE;;;;;;;;GAQG;AACH,eAAO,MAAM,6BAA6B,QAAQ,CAAA;AAElD,+EAA+E;AAC/E,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,iBAAiB,EACxB,OAAO,GAAE;IAAC,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAAC,kBAAkB,CAAC,EAAE,OAAO,CAAA;CAAM,GAC5E,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"slimStreamStatus.d.ts","sourceRoot":"","sources":["../../src/services/slimStreamStatus.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,gCAAgC,CAAA;AAErE;;;;;;;;GAQG;AACH,eAAO,MAAM,6BAA6B,QAAQ,CAAA;AAElD,+EAA+E;AAC/E,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,iBAAiB,EACxB,OAAO,GAAE;IAAC,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAAC,kBAAkB,CAAC,EAAE,OAAO,CAAA;CAAM,GAC5E,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA0BzB;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAE9E"}
|
|
@@ -18,6 +18,22 @@ export function slimStreamStatusEvent(event, options = {}) {
|
|
|
18
18
|
};
|
|
19
19
|
if (event.streamId)
|
|
20
20
|
slim.streamId = event.streamId;
|
|
21
|
+
if (event.sid)
|
|
22
|
+
slim.sid = event.sid;
|
|
23
|
+
if (typeof event.revision === "number")
|
|
24
|
+
slim.revision = event.revision;
|
|
25
|
+
if (typeof event.terminal === "boolean")
|
|
26
|
+
slim.terminal = event.terminal;
|
|
27
|
+
if (event.errorDetails)
|
|
28
|
+
slim.errorDetails = event.errorDetails;
|
|
29
|
+
if (event.kind === "reconnect") {
|
|
30
|
+
if ("reason" in event)
|
|
31
|
+
slim.reason = event.reason;
|
|
32
|
+
if ("attempt" in event)
|
|
33
|
+
slim.attempt = event.attempt;
|
|
34
|
+
if ("maxAttempts" in event)
|
|
35
|
+
slim.maxAttempts = event.maxAttempts;
|
|
36
|
+
}
|
|
21
37
|
const ts = event.timestamp;
|
|
22
38
|
if (typeof ts === "number" && Number.isFinite(ts))
|
|
23
39
|
slim.timestamp = ts;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slimStreamStatus.js","sourceRoot":"","sources":["../../src/services/slimStreamStatus.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAG,KAAK,CAAA;AAElD,+EAA+E;AAC/E,MAAM,UAAU,qBAAqB,CACnC,KAAwB,EACxB,UAA2E,EAAE;IAE7E,MAAM,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,IAAI,6BAA6B,CAAA;IACtF,MAAM,IAAI,GAA4B;QACpC,IAAI,EAAE,eAAe;QACrB,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,MAAM,EAAE,KAAK,CAAC,MAAM;KACrB,CAAA;IACD,IAAI,KAAK,CAAC,QAAQ;QAAE,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAA;IAClD,MAAM,EAAE,GAAG,KAAK,CAAC,SAAS,CAAA;IAC1B,IAAI,OAAO,EAAE,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;QAAE,IAAI,CAAC,SAAS,GAAG,EAAE,CAAA;IACtE,IAAI,OAAO,CAAC,qBAAqB,IAAI,KAAK,CAAC,cAAc,EAAE,CAAC;QAC1D,IAAI,CAAC,cAAc,GAAG,KAAK,CAAC,cAAc,CAAA;IAC5C,CAAC;IACD,+EAA+E;IAC/E,kFAAkF;IAClF,IAAI,kBAAkB,IAAI,KAAK,CAAC,KAAK;QAAE,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAA;IAC/D,OAAO,IAAI,CAAA;AACb,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,OAAgC;IACpE,OAAO,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAA;AAChC,CAAC","sourcesContent":["import type {StreamStatusEvent} from \"@mentra/bluetooth-sdk/internal\"\r\n\r\n/**\r\n * 1Hz encoder FPS/bitrate telemetry on `stream_status.stats`.\r\n * Lifecycle status (started/stopped/error) is unaffected.\r\n * Keep false in production; flip locally to debug the Mentra Call FPS ladder.\r\n *\r\n * Compatibility kill switch: old glasses can still emit stats. Copy onto a new object;\r\n * never mutate the incoming event. Manual acceptance with every layer false: join\r\n * waterfall yes; BLE stream_status.stats / STREAM_QUALITY / encoder-stats / watch-stats no.\r\n */\r\nexport const ENABLE_PIPELINE_FPS_TELEMETRY = false\r\n\r\n/** Fields forwarded to cloud / miniapps after the first resolvedConfig ack. */\r\nexport function slimStreamStatusEvent(\r\n event: StreamStatusEvent,\r\n options: {includeResolvedConfig?: boolean; enableFpsTelemetry?: boolean} = {},\r\n): Record<string, unknown> {\r\n const enableFpsTelemetry = options.enableFpsTelemetry ?? ENABLE_PIPELINE_FPS_TELEMETRY\r\n const slim: Record<string, unknown> = {\r\n type: \"stream_status\",\r\n kind: event.kind,\r\n status: event.status,\r\n }\r\n if (event.streamId) slim.streamId = event.streamId\
|
|
1
|
+
{"version":3,"file":"slimStreamStatus.js","sourceRoot":"","sources":["../../src/services/slimStreamStatus.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAG,KAAK,CAAA;AAElD,+EAA+E;AAC/E,MAAM,UAAU,qBAAqB,CACnC,KAAwB,EACxB,UAA2E,EAAE;IAE7E,MAAM,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,IAAI,6BAA6B,CAAA;IACtF,MAAM,IAAI,GAA4B;QACpC,IAAI,EAAE,eAAe;QACrB,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,MAAM,EAAE,KAAK,CAAC,MAAM;KACrB,CAAA;IACD,IAAI,KAAK,CAAC,QAAQ;QAAE,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAA;IAClD,IAAI,KAAK,CAAC,GAAG;QAAE,IAAI,CAAC,GAAG,GAAG,KAAK,CAAC,GAAG,CAAA;IACnC,IAAI,OAAO,KAAK,CAAC,QAAQ,KAAK,QAAQ;QAAE,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAA;IACtE,IAAI,OAAO,KAAK,CAAC,QAAQ,KAAK,SAAS;QAAE,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAA;IACvE,IAAI,KAAK,CAAC,YAAY;QAAE,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC,YAAY,CAAA;IAC9D,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QAC/B,IAAI,QAAQ,IAAI,KAAK;YAAE,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAA;QACjD,IAAI,SAAS,IAAI,KAAK;YAAE,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAA;QACpD,IAAI,aAAa,IAAI,KAAK;YAAE,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC,WAAW,CAAA;IAClE,CAAC;IACD,MAAM,EAAE,GAAG,KAAK,CAAC,SAAS,CAAA;IAC1B,IAAI,OAAO,EAAE,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;QAAE,IAAI,CAAC,SAAS,GAAG,EAAE,CAAA;IACtE,IAAI,OAAO,CAAC,qBAAqB,IAAI,KAAK,CAAC,cAAc,EAAE,CAAC;QAC1D,IAAI,CAAC,cAAc,GAAG,KAAK,CAAC,cAAc,CAAA;IAC5C,CAAC;IACD,+EAA+E;IAC/E,kFAAkF;IAClF,IAAI,kBAAkB,IAAI,KAAK,CAAC,KAAK;QAAE,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAA;IAC/D,OAAO,IAAI,CAAA;AACb,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,OAAgC;IACpE,OAAO,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAA;AAChC,CAAC","sourcesContent":["import type {StreamStatusEvent} from \"@mentra/bluetooth-sdk/internal\"\r\n\r\n/**\r\n * 1Hz encoder FPS/bitrate telemetry on `stream_status.stats`.\r\n * Lifecycle status (started/stopped/error) is unaffected.\r\n * Keep false in production; flip locally to debug the Mentra Call FPS ladder.\r\n *\r\n * Compatibility kill switch: old glasses can still emit stats. Copy onto a new object;\r\n * never mutate the incoming event. Manual acceptance with every layer false: join\r\n * waterfall yes; BLE stream_status.stats / STREAM_QUALITY / encoder-stats / watch-stats no.\r\n */\r\nexport const ENABLE_PIPELINE_FPS_TELEMETRY = false\r\n\r\n/** Fields forwarded to cloud / miniapps after the first resolvedConfig ack. */\r\nexport function slimStreamStatusEvent(\r\n event: StreamStatusEvent,\r\n options: {includeResolvedConfig?: boolean; enableFpsTelemetry?: boolean} = {},\r\n): Record<string, unknown> {\r\n const enableFpsTelemetry = options.enableFpsTelemetry ?? ENABLE_PIPELINE_FPS_TELEMETRY\r\n const slim: Record<string, unknown> = {\r\n type: \"stream_status\",\r\n kind: event.kind,\r\n status: event.status,\r\n }\r\n if (event.streamId) slim.streamId = event.streamId\n if (event.sid) slim.sid = event.sid\n if (typeof event.revision === \"number\") slim.revision = event.revision\n if (typeof event.terminal === \"boolean\") slim.terminal = event.terminal\n if (event.errorDetails) slim.errorDetails = event.errorDetails\n if (event.kind === \"reconnect\") {\n if (\"reason\" in event) slim.reason = event.reason\n if (\"attempt\" in event) slim.attempt = event.attempt\n if (\"maxAttempts\" in event) slim.maxAttempts = event.maxAttempts\n }\n const ts = event.timestamp\r\n if (typeof ts === \"number\" && Number.isFinite(ts)) slim.timestamp = ts\r\n if (options.includeResolvedConfig && event.resolvedConfig) {\r\n slim.resolvedConfig = event.resolvedConfig\r\n }\r\n // Compatibility kill switch: old glasses may still emit 1Hz stats. Copy onto a\r\n // new object; never mutate `event` — other consumers may still need the original.\r\n if (enableFpsTelemetry && event.stats) slim.stats = event.stats\r\n return slim\r\n}\r\n\r\nexport function streamStatusSignature(payload: Record<string, unknown>): string {\r\n return JSON.stringify(payload)\r\n}\r\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mentra/engine",
|
|
3
|
-
"version": "3.2.0-dev.
|
|
3
|
+
"version": "3.2.0-dev.208",
|
|
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,12 +97,12 @@
|
|
|
97
97
|
"registry": "https://registry.npmjs.org/"
|
|
98
98
|
},
|
|
99
99
|
"dependencies": {
|
|
100
|
-
"@mentra/acs-meeting": "3.2.0-dev.
|
|
101
|
-
"@mentra/bluetooth-sdk": "3.2.0-dev.
|
|
102
|
-
"@mentra/cloud-client": "3.2.0-dev.
|
|
103
|
-
"@mentra/cloud-protocol": "3.2.0-dev.
|
|
104
|
-
"@mentra/crust": "3.2.0-dev.
|
|
105
|
-
"@mentra/miniapp": "3.2.0-dev.
|
|
100
|
+
"@mentra/acs-meeting": "3.2.0-dev.208",
|
|
101
|
+
"@mentra/bluetooth-sdk": "3.2.0-dev.208",
|
|
102
|
+
"@mentra/cloud-client": "3.2.0-dev.208",
|
|
103
|
+
"@mentra/cloud-protocol": "3.2.0-dev.208",
|
|
104
|
+
"@mentra/crust": "3.2.0-dev.208",
|
|
105
|
+
"@mentra/miniapp": "3.2.0-dev.208",
|
|
106
106
|
"buffer": "^6.0.3",
|
|
107
107
|
"events": "^3.3.0",
|
|
108
108
|
"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.0",
|
|
15
|
-
"releaseIdentity": "3.2.0-dev.
|
|
16
|
-
"releaseSetId": "mentra-3.2.0-dev.
|
|
17
|
-
"sourceCommit": "
|
|
18
|
-
"otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.
|
|
19
|
-
"otaManifestSha256": "
|
|
15
|
+
"releaseIdentity": "3.2.0-dev.208",
|
|
16
|
+
"releaseSetId": "mentra-3.2.0-dev.208",
|
|
17
|
+
"sourceCommit": "3c22b92a5a53ce608c087503417ca5b3d16cc926",
|
|
18
|
+
"otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.208.json",
|
|
19
|
+
"otaManifestSha256": "69ce2b786afea60faaa5a47aad1ff0c9e70933365658b6628e651fb0c6cfe7bd"
|
|
20
20
|
})
|
|
@@ -162,22 +162,18 @@ export function startDeviceEventRouter(): void {
|
|
|
162
162
|
}),
|
|
163
163
|
)
|
|
164
164
|
|
|
165
|
-
// Phone-owned stream status
|
|
165
|
+
// Phone-owned stream status → the stream coordinator. The Cloud V1 relay
|
|
166
166
|
// for non-owned streams was removed with Cloud V1 app end-of-life.
|
|
167
167
|
subs.push(
|
|
168
168
|
BluetoothSdk.addListener("stream_status", (event) => {
|
|
169
|
-
if (
|
|
169
|
+
if (
|
|
170
|
+
(event.streamId && phoneStreamCoordinator.owns(event.streamId)) ||
|
|
171
|
+
(!event.streamId && event.kind === "snapshot" && event.terminal === true)
|
|
172
|
+
) {
|
|
170
173
|
phoneStreamCoordinator.handleGlassesStatus(event)
|
|
171
174
|
}
|
|
172
175
|
}),
|
|
173
176
|
)
|
|
174
|
-
subs.push(
|
|
175
|
-
BluetoothSdk.addListener("keep_alive_ack", (event) => {
|
|
176
|
-
if (event.streamId && phoneStreamCoordinator.owns(event.streamId)) {
|
|
177
|
-
phoneStreamCoordinator.handleKeepAliveAck(event)
|
|
178
|
-
}
|
|
179
|
-
}),
|
|
180
|
-
)
|
|
181
177
|
|
|
182
178
|
// --- device input → local miniapps (forwardEvent is subscriber-gated; a no-op when
|
|
183
179
|
// no miniapp listens). The v1 SocketComms cloud legs for these were removed with
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
* miniapp → SDK → LocalMiniappRuntime → coordinator
|
|
6
6
|
* coordinator → BluetoothSdk (BLE → glasses publisher)
|
|
7
7
|
* coordinator ↔ cloudStreamApi (managed only, cloud-v2 runtime provisioning)
|
|
8
|
-
* coordinator ↔ StreamLifecycleController (keep-alive heartbeat)
|
|
9
8
|
* coordinator → status listeners → routed back to miniapp(s)
|
|
10
9
|
*
|
|
11
10
|
* Single-stream constraint:
|
|
@@ -28,29 +27,17 @@
|
|
|
28
27
|
* the legacy path.
|
|
29
28
|
*
|
|
30
29
|
* BLE link loss is a SUSPENDED state, not a failure:
|
|
31
|
-
*
|
|
32
|
-
* link
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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.
|
|
30
|
+
* ASG owns the ten-second phone/controller liveness deadline. The coordinator
|
|
31
|
+
* observes link suspension, retains cloud resources briefly for reconciliation,
|
|
32
|
+
* and never uses missing JavaScript heartbeats as evidence of publisher failure.
|
|
33
|
+
* An explicit stop that could not reach BLE is sent on the next reconnect.
|
|
41
34
|
*/
|
|
42
35
|
|
|
43
36
|
import BluetoothSdk from "@mentra/bluetooth-sdk/internal"
|
|
44
|
-
import type {
|
|
45
|
-
KeepAliveAckEvent,
|
|
46
|
-
StreamResolvedConfig,
|
|
47
|
-
StreamStartRequest,
|
|
48
|
-
StreamStatusEvent,
|
|
49
|
-
} from "@mentra/bluetooth-sdk/internal"
|
|
37
|
+
import type {StreamResolvedConfig, StreamStartRequest, StreamStatusEvent} from "@mentra/bluetooth-sdk/internal"
|
|
50
38
|
import {isGlassesConnected} from "./GlassesReadiness"
|
|
51
39
|
import {useGlassesStore} from "../stores/glasses"
|
|
52
40
|
|
|
53
|
-
import {StreamLifecycleController, type LifecycleLogger} from "./StreamLifecycleController"
|
|
54
41
|
import {slimStreamStatusEvent, streamStatusSignature} from "./slimStreamStatus"
|
|
55
42
|
import {
|
|
56
43
|
getManagedStreamStatus,
|
|
@@ -66,9 +53,6 @@ import {
|
|
|
66
53
|
* can shorten them; production code should never override these.
|
|
67
54
|
*/
|
|
68
55
|
const DEFAULT_TIMINGS = {
|
|
69
|
-
keepAliveIntervalMs: 15_000,
|
|
70
|
-
ackTimeoutMs: 10_000,
|
|
71
|
-
maxMissedAcks: 3,
|
|
72
56
|
cloudflareStatusPollMs: 5_000,
|
|
73
57
|
// During WHIP startup, probe quickly so readiness is not quantized to the
|
|
74
58
|
// steady-state 5s monitoring cadence. The delay backs off on each miss.
|
|
@@ -77,10 +61,8 @@ const DEFAULT_TIMINGS = {
|
|
|
77
61
|
hlsReadinessInitialDelayMs: 5_000,
|
|
78
62
|
hlsReadinessPollMs: 2_000,
|
|
79
63
|
hlsReadinessMaxAttempts: 30,
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
// margin for the resume heartbeat's round trip.
|
|
83
|
-
glassesGraceMs: 45_000,
|
|
64
|
+
// Allow ASG's ten-second stop plus a short delivery/reconciliation margin.
|
|
65
|
+
glassesGraceMs: 15_000,
|
|
84
66
|
// Consecutive Cloudflare "publisher disconnected" probes while suspended
|
|
85
67
|
// before we conclude the glasses are off (not just out of BLE range).
|
|
86
68
|
suspendedPublisherGoneProbes: 2,
|
|
@@ -111,19 +93,6 @@ export const LINK_STATUS = {
|
|
|
111
93
|
reason: "glasses_disconnected",
|
|
112
94
|
} as const
|
|
113
95
|
|
|
114
|
-
// Console-backed minimal logger; replaces pino on the phone.
|
|
115
|
-
const consoleLogger: LifecycleLogger = {
|
|
116
|
-
child: (bindings) => ({
|
|
117
|
-
...consoleLogger,
|
|
118
|
-
debug: (...args) => console.debug("[STREAM]", bindings, ...args),
|
|
119
|
-
warn: (...args) => console.warn("[STREAM]", bindings, ...args),
|
|
120
|
-
error: (...args) => console.error("[STREAM]", bindings, ...args),
|
|
121
|
-
}),
|
|
122
|
-
debug: (...args) => console.debug("[STREAM]", ...args),
|
|
123
|
-
warn: (...args) => console.warn("[STREAM]", ...args),
|
|
124
|
-
error: (...args) => console.error("[STREAM]", ...args),
|
|
125
|
-
}
|
|
126
|
-
|
|
127
96
|
export interface StartUnmanagedOptions {
|
|
128
97
|
streamUrl: string
|
|
129
98
|
video?: StreamStartRequest["video"]
|
|
@@ -237,7 +206,6 @@ interface SuspendedState {
|
|
|
237
206
|
|
|
238
207
|
export class PhoneStreamCoordinator {
|
|
239
208
|
private current: Entry | null = null
|
|
240
|
-
private lifecycle: StreamLifecycleController | null = null
|
|
241
209
|
private statusSubscriber: StatusSubscriber | null = null
|
|
242
210
|
private idCounter = 0
|
|
243
211
|
private readonly timings: TimingConfig
|
|
@@ -277,12 +245,7 @@ export class PhoneStreamCoordinator {
|
|
|
277
245
|
*/
|
|
278
246
|
private assertGlassesConnected(): void {
|
|
279
247
|
if (!this.linkSource.isConnected()) {
|
|
280
|
-
throw new StreamConflictError(
|
|
281
|
-
"GLASSES_NOT_CONNECTED",
|
|
282
|
-
"Glasses are not connected",
|
|
283
|
-
"command",
|
|
284
|
-
"ble",
|
|
285
|
-
)
|
|
248
|
+
throw new StreamConflictError("GLASSES_NOT_CONNECTED", "Glasses are not connected", "command", "ble")
|
|
286
249
|
}
|
|
287
250
|
}
|
|
288
251
|
|
|
@@ -352,10 +315,7 @@ export class PhoneStreamCoordinator {
|
|
|
352
315
|
}
|
|
353
316
|
}
|
|
354
317
|
|
|
355
|
-
async startUnmanaged(
|
|
356
|
-
packageName: string,
|
|
357
|
-
opts: StartUnmanagedOptions,
|
|
358
|
-
): Promise<StreamPublisherStartResult> {
|
|
318
|
+
async startUnmanaged(packageName: string, opts: StartUnmanagedOptions): Promise<StreamPublisherStartResult> {
|
|
359
319
|
// Pre-check the obvious-bad input before queueing — the lock is for
|
|
360
320
|
// serializing state transitions, not for validating arguments.
|
|
361
321
|
if (!opts.streamUrl || typeof opts.streamUrl !== "string") {
|
|
@@ -382,7 +342,7 @@ export class PhoneStreamCoordinator {
|
|
|
382
342
|
this.current = entry
|
|
383
343
|
|
|
384
344
|
try {
|
|
385
|
-
const event = await BluetoothSdk.
|
|
345
|
+
const event = await BluetoothSdk.startStream({
|
|
386
346
|
type: "start_stream",
|
|
387
347
|
streamUrl: opts.streamUrl,
|
|
388
348
|
streamId,
|
|
@@ -404,10 +364,7 @@ export class PhoneStreamCoordinator {
|
|
|
404
364
|
})
|
|
405
365
|
}
|
|
406
366
|
|
|
407
|
-
async startManaged(
|
|
408
|
-
packageName: string,
|
|
409
|
-
opts: StartManagedOptions,
|
|
410
|
-
): Promise<ManagedStartResult> {
|
|
367
|
+
async startManaged(packageName: string, opts: StartManagedOptions): Promise<ManagedStartResult> {
|
|
411
368
|
const startupStartedAtMs = Date.now()
|
|
412
369
|
// streamId doesn't exist yet — it's minted a few lines below, once we
|
|
413
370
|
// know this is a fresh provision rather than a join onto an existing one.
|
|
@@ -446,9 +403,7 @@ export class PhoneStreamCoordinator {
|
|
|
446
403
|
)
|
|
447
404
|
}
|
|
448
405
|
existing.subscribers.add(packageName)
|
|
449
|
-
const immediate: ManagedStartResult | null = existing.hlsReady
|
|
450
|
-
? managedStartResult(existing)
|
|
451
|
-
: null
|
|
406
|
+
const immediate: ManagedStartResult | null = existing.hlsReady ? managedStartResult(existing) : null
|
|
452
407
|
return {kind: "join", entry: existing, immediate}
|
|
453
408
|
}
|
|
454
409
|
|
|
@@ -458,8 +413,7 @@ export class PhoneStreamCoordinator {
|
|
|
458
413
|
const provision = await provisionManagedStream(opts.restreamDestinations)
|
|
459
414
|
const streamId = this.mintId("m")
|
|
460
415
|
const ingestUrl = pickIngestUrl(provision, opts.ingest)
|
|
461
|
-
const mode: ManagedEntry["mode"] =
|
|
462
|
-
ingestUrl === provision.webrtcPublishUrl ? "webrtc" : "hls"
|
|
416
|
+
const mode: ManagedEntry["mode"] = ingestUrl === provision.webrtcPublishUrl ? "webrtc" : "hls"
|
|
463
417
|
|
|
464
418
|
const entry: ManagedEntry = {
|
|
465
419
|
kind: "managed",
|
|
@@ -488,7 +442,7 @@ export class PhoneStreamCoordinator {
|
|
|
488
442
|
})
|
|
489
443
|
|
|
490
444
|
try {
|
|
491
|
-
const event = await BluetoothSdk.
|
|
445
|
+
const event = await BluetoothSdk.startStream({
|
|
492
446
|
type: "start_stream",
|
|
493
447
|
streamUrl: ingestUrl,
|
|
494
448
|
streamId,
|
|
@@ -573,7 +527,6 @@ export class PhoneStreamCoordinator {
|
|
|
573
527
|
if (!this.current) return
|
|
574
528
|
if (event.streamId && event.streamId !== this.current.streamId) return
|
|
575
529
|
|
|
576
|
-
this.lifecycle?.recordActivity()
|
|
577
530
|
const includeResolvedConfig = !this.resolvedConfigForwarded && !!event.resolvedConfig
|
|
578
531
|
if (includeResolvedConfig) this.resolvedConfigForwarded = true
|
|
579
532
|
const slimData = slimStreamStatusEvent(event, {includeResolvedConfig})
|
|
@@ -593,15 +546,14 @@ export class PhoneStreamCoordinator {
|
|
|
593
546
|
// glasses publisher auto-recovers (error → reconnecting → reconnected),
|
|
594
547
|
// and tearing down on the first hiccup deletes the live input out from
|
|
595
548
|
// under a publisher that comes right back (it then retries into a dead
|
|
596
|
-
// input forever).
|
|
597
|
-
//
|
|
598
|
-
// lock so it serializes with any start/stop currently in flight.
|
|
549
|
+
// input forever). ASG explicitly marks terminal failures; serialize teardown
|
|
550
|
+
// with any start/stop currently in flight.
|
|
599
551
|
const isStopped =
|
|
600
552
|
(event.kind === "lifecycle" && event.status === "stopped") ||
|
|
601
553
|
(event.kind === "snapshot" && event.status === "stopped")
|
|
602
554
|
const isGiveUp = event.kind === "reconnect" && event.status === "reconnect_failed"
|
|
603
|
-
if (isGiveUp || isStopped) {
|
|
604
|
-
const reason = isGiveUp ? "glasses_gave_up" : "glasses_stopped"
|
|
555
|
+
if (event.terminal === true || isGiveUp || isStopped) {
|
|
556
|
+
const reason = isGiveUp ? "glasses_gave_up" : event.status === "error" ? "glasses_error" : "glasses_stopped"
|
|
605
557
|
const targetStreamId = this.current.streamId
|
|
606
558
|
void this.runExclusive(async () => {
|
|
607
559
|
// The stream we wanted to tear down may already be gone (e.g. another
|
|
@@ -612,12 +564,6 @@ export class PhoneStreamCoordinator {
|
|
|
612
564
|
}
|
|
613
565
|
}
|
|
614
566
|
|
|
615
|
-
/** Called by MantleManager when a phone-owned keep_alive_ack arrives. */
|
|
616
|
-
handleKeepAliveAck(event: KeepAliveAckEvent): void {
|
|
617
|
-
if (!event.ackId) return
|
|
618
|
-
this.lifecycle?.handleAck(event.ackId)
|
|
619
|
-
}
|
|
620
|
-
|
|
621
567
|
// ===========================================================================
|
|
622
568
|
// BLE link suspension
|
|
623
569
|
// ===========================================================================
|
|
@@ -649,9 +595,6 @@ export class PhoneStreamCoordinator {
|
|
|
649
595
|
const entry = this.current
|
|
650
596
|
if (!entry) return
|
|
651
597
|
const since = Date.now()
|
|
652
|
-
// Pause the heartbeat: misses now would be our own fault, not the
|
|
653
|
-
// publisher's, and counting them would reap a stream that is still live.
|
|
654
|
-
this.lifecycle?.setActive(false)
|
|
655
598
|
const graceTimer = setTimeout(() => this.onGraceExpired(entry.streamId), this.timings.glassesGraceMs)
|
|
656
599
|
this.suspended = {since, graceTimer, publisherGoneProbes: 0}
|
|
657
600
|
console.warn("[STREAM] BLE link lost; stream suspended", {
|
|
@@ -674,10 +617,6 @@ export class PhoneStreamCoordinator {
|
|
|
674
617
|
this.suspended = null
|
|
675
618
|
const suspendedMs = Date.now() - suspended.since
|
|
676
619
|
console.info("[STREAM] BLE link back; stream resumed", {streamId: entry.streamId, suspendedMs})
|
|
677
|
-
// Resume and heartbeat immediately: the glasses watchdog has been running
|
|
678
|
-
// the whole time, so the first keep-alive must not wait another interval.
|
|
679
|
-
this.lifecycle?.setActive(true)
|
|
680
|
-
this.lifecycle?.tickNow()
|
|
681
620
|
// A resumed session is a fresh status baseline for subscribers.
|
|
682
621
|
this.lastFanoutSignature = null
|
|
683
622
|
this.fanout({
|
|
@@ -730,45 +669,10 @@ export class PhoneStreamCoordinator {
|
|
|
730
669
|
return `phone-${prefix}-${Date.now().toString(36)}-${this.idCounter}`
|
|
731
670
|
}
|
|
732
671
|
|
|
733
|
-
/**
|
|
734
|
-
private startLifecycle(
|
|
735
|
-
// A new stream supersedes any deferred stop for the previous one.
|
|
672
|
+
/** Observe link/status; native SDK and ASG own controller liveness. */
|
|
673
|
+
private startLifecycle(_streamId: string): void {
|
|
736
674
|
this.pendingBleStop = null
|
|
737
675
|
this.attachLink()
|
|
738
|
-
this.lifecycle?.dispose()
|
|
739
|
-
const ctrl = new StreamLifecycleController(
|
|
740
|
-
{
|
|
741
|
-
logger: consoleLogger,
|
|
742
|
-
streamId,
|
|
743
|
-
keepAliveIntervalMs: this.timings.keepAliveIntervalMs,
|
|
744
|
-
ackTimeoutMs: this.timings.ackTimeoutMs,
|
|
745
|
-
maxMissedAcks: this.timings.maxMissedAcks,
|
|
746
|
-
},
|
|
747
|
-
{
|
|
748
|
-
sendKeepAlive: async (ackId) => {
|
|
749
|
-
await BluetoothSdk.sendExternallyManagedStreamKeepAlive({
|
|
750
|
-
type: "keep_stream_alive",
|
|
751
|
-
streamId,
|
|
752
|
-
ackId,
|
|
753
|
-
})
|
|
754
|
-
},
|
|
755
|
-
onTimeout: async () => {
|
|
756
|
-
this.fanout({
|
|
757
|
-
streamId,
|
|
758
|
-
source: "coordinator",
|
|
759
|
-
status: "error",
|
|
760
|
-
data: {reason: "keep_alive_timeout"},
|
|
761
|
-
})
|
|
762
|
-
await this.runExclusive(async () => {
|
|
763
|
-
// The stream this timeout was bound to may already be gone.
|
|
764
|
-
if (this.current?.streamId !== streamId) return
|
|
765
|
-
await this.teardownLocked("keep_alive_timeout")
|
|
766
|
-
})
|
|
767
|
-
},
|
|
768
|
-
},
|
|
769
|
-
)
|
|
770
|
-
ctrl.setActive(true)
|
|
771
|
-
this.lifecycle = ctrl
|
|
772
676
|
}
|
|
773
677
|
|
|
774
678
|
private startCloudflareStatusPoll(entry: ManagedEntry): void {
|
|
@@ -967,10 +871,7 @@ export class PhoneStreamCoordinator {
|
|
|
967
871
|
|
|
968
872
|
private fanout(update: StreamStatusUpdate): void {
|
|
969
873
|
if (!this.current || !this.statusSubscriber) return
|
|
970
|
-
const targets =
|
|
971
|
-
this.current.kind === "managed"
|
|
972
|
-
? Array.from(this.current.subscribers)
|
|
973
|
-
: [this.current.packageName]
|
|
874
|
+
const targets = this.current.kind === "managed" ? Array.from(this.current.subscribers) : [this.current.packageName]
|
|
974
875
|
for (const pkg of targets) {
|
|
975
876
|
try {
|
|
976
877
|
this.statusSubscriber(pkg, update)
|
|
@@ -1000,12 +901,6 @@ export class PhoneStreamCoordinator {
|
|
|
1000
901
|
this.suspended = null
|
|
1001
902
|
}
|
|
1002
903
|
|
|
1003
|
-
// Dispose the lifecycle controller immediately so it doesn't fire one
|
|
1004
|
-
// more keep-alive against a stream we're tearing down. The transition
|
|
1005
|
-
// lock guarantees no new lifecycle is started concurrently.
|
|
1006
|
-
this.lifecycle?.dispose()
|
|
1007
|
-
this.lifecycle = null
|
|
1008
|
-
|
|
1009
904
|
// With the link down a BLE write can only fail (and hold the transition
|
|
1010
905
|
// lock for the native timeout). Defer it to the next reconnect instead.
|
|
1011
906
|
const linkUp = this.linkSource.isConnected()
|
|
@@ -1075,8 +970,8 @@ function pickIngestUrl(p: ProvisionResult, preference?: "srt" | "whip" | "rtmp")
|
|
|
1075
970
|
preference === "whip"
|
|
1076
971
|
? p.webrtcPublishUrl || p.srtUrl || p.rtmpUrl
|
|
1077
972
|
: preference === "rtmp"
|
|
1078
|
-
|
|
1079
|
-
|
|
973
|
+
? p.rtmpUrl || p.srtUrl || p.webrtcPublishUrl
|
|
974
|
+
: p.srtUrl || p.rtmpUrl || p.webrtcPublishUrl
|
|
1080
975
|
if (!url) {
|
|
1081
976
|
throw new Error("Cloudflare provision returned no usable ingest URL")
|
|
1082
977
|
}
|