@mentra/miniapp 3.2.1-beta.319 → 3.2.1-beta.355

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.
Files changed (102) hide show
  1. package/README.md +16 -0
  2. package/dist/background/index.d.ts +5 -3
  3. package/dist/background/index.d.ts.map +1 -1
  4. package/dist/background/index.js +1 -0
  5. package/dist/background/index.js.map +1 -1
  6. package/dist/index.d.ts +6 -3
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +2 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/modules/display.d.ts +33 -0
  11. package/dist/modules/display.d.ts.map +1 -1
  12. package/dist/modules/display.js +2 -0
  13. package/dist/modules/display.js.map +1 -1
  14. package/dist/modules/meeting.d.ts +66 -6
  15. package/dist/modules/meeting.d.ts.map +1 -1
  16. package/dist/modules/meeting.js +72 -19
  17. package/dist/modules/meeting.js.map +1 -1
  18. package/dist/modules/stream.d.ts +46 -0
  19. package/dist/modules/stream.d.ts.map +1 -1
  20. package/dist/modules/stream.js +107 -0
  21. package/dist/modules/stream.js.map +1 -1
  22. package/dist/protocol.d.ts +39 -0
  23. package/dist/protocol.d.ts.map +1 -1
  24. package/dist/protocol.js +36 -0
  25. package/dist/protocol.js.map +1 -1
  26. package/dist/react/StreamPreview.d.ts +41 -0
  27. package/dist/react/StreamPreview.d.ts.map +1 -0
  28. package/dist/react/StreamPreview.js +144 -0
  29. package/dist/react/StreamPreview.js.map +1 -0
  30. package/dist/react/index.d.ts +2 -0
  31. package/dist/react/index.d.ts.map +1 -1
  32. package/dist/react/index.js +1 -0
  33. package/dist/react/index.js.map +1 -1
  34. package/dist/react/stream-preview/PreviewConnection.d.ts +139 -0
  35. package/dist/react/stream-preview/PreviewConnection.d.ts.map +1 -0
  36. package/dist/react/stream-preview/PreviewConnection.js +462 -0
  37. package/dist/react/stream-preview/PreviewConnection.js.map +1 -0
  38. package/dist/react/stream-preview/controlChannel.d.ts +75 -0
  39. package/dist/react/stream-preview/controlChannel.d.ts.map +1 -0
  40. package/dist/react/stream-preview/controlChannel.js +55 -0
  41. package/dist/react/stream-preview/controlChannel.js.map +1 -0
  42. package/dist/react/stream-preview/creditLoop.d.ts +81 -0
  43. package/dist/react/stream-preview/creditLoop.d.ts.map +1 -0
  44. package/dist/react/stream-preview/creditLoop.js +146 -0
  45. package/dist/react/stream-preview/creditLoop.js.map +1 -0
  46. package/dist/react/stream-preview/protocol.d.ts +113 -0
  47. package/dist/react/stream-preview/protocol.d.ts.map +1 -0
  48. package/dist/react/stream-preview/protocol.js +148 -0
  49. package/dist/react/stream-preview/protocol.js.map +1 -0
  50. package/dist/react/stream-preview/resizeCoalescer.d.ts +38 -0
  51. package/dist/react/stream-preview/resizeCoalescer.d.ts.map +1 -0
  52. package/dist/react/stream-preview/resizeCoalescer.js +71 -0
  53. package/dist/react/stream-preview/resizeCoalescer.js.map +1 -0
  54. package/dist/react/stream-preview/testFrames.d.ts +38 -0
  55. package/dist/react/stream-preview/testFrames.d.ts.map +1 -0
  56. package/dist/react/stream-preview/testFrames.js +50 -0
  57. package/dist/react/stream-preview/testFrames.js.map +1 -0
  58. package/dist/react/stream-preview/tiers.d.ts +21 -0
  59. package/dist/react/stream-preview/tiers.d.ts.map +1 -0
  60. package/dist/react/stream-preview/tiers.js +33 -0
  61. package/dist/react/stream-preview/tiers.js.map +1 -0
  62. package/dist/react/stream-preview/trace.d.ts +23 -0
  63. package/dist/react/stream-preview/trace.d.ts.map +1 -0
  64. package/dist/react/stream-preview/trace.js +71 -0
  65. package/dist/react/stream-preview/trace.js.map +1 -0
  66. package/dist/react/stream-preview/transport.d.ts +79 -0
  67. package/dist/react/stream-preview/transport.d.ts.map +1 -0
  68. package/dist/react/stream-preview/transport.js +209 -0
  69. package/dist/react/stream-preview/transport.js.map +1 -0
  70. package/dist/react/stream-preview/yuvRenderer.d.ts +31 -0
  71. package/dist/react/stream-preview/yuvRenderer.d.ts.map +1 -0
  72. package/dist/react/stream-preview/yuvRenderer.js +398 -0
  73. package/dist/react/stream-preview/yuvRenderer.js.map +1 -0
  74. package/dist/session.d.ts +5 -0
  75. package/dist/session.d.ts.map +1 -1
  76. package/dist/session.js +5 -0
  77. package/dist/session.js.map +1 -1
  78. package/dist/ui/index.d.ts +2 -0
  79. package/dist/ui/index.d.ts.map +1 -1
  80. package/dist/ui/index.js +1 -0
  81. package/dist/ui/index.js.map +1 -1
  82. package/package.json +8 -3
  83. package/src/background/index.ts +14 -1
  84. package/src/index.ts +15 -0
  85. package/src/modules/display.ts +37 -0
  86. package/src/modules/meeting.ts +133 -23
  87. package/src/modules/stream.ts +153 -1
  88. package/src/protocol.ts +75 -0
  89. package/src/react/StreamPreview.tsx +187 -0
  90. package/src/react/index.ts +2 -0
  91. package/src/react/stream-preview/PreviewConnection.ts +552 -0
  92. package/src/react/stream-preview/controlChannel.ts +119 -0
  93. package/src/react/stream-preview/creditLoop.ts +189 -0
  94. package/src/react/stream-preview/protocol.ts +227 -0
  95. package/src/react/stream-preview/resizeCoalescer.ts +88 -0
  96. package/src/react/stream-preview/testFrames.ts +86 -0
  97. package/src/react/stream-preview/tiers.ts +37 -0
  98. package/src/react/stream-preview/trace.ts +81 -0
  99. package/src/react/stream-preview/transport.ts +242 -0
  100. package/src/react/stream-preview/yuvRenderer.ts +453 -0
  101. package/src/session.ts +8 -0
  102. package/src/ui/index.ts +2 -0
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Page-side `PREVIEW_TRACE` lines.
3
+ *
4
+ * Same shape as the host and native layers: a literal grep marker, `phase=`, then `key=value`
5
+ * pairs. The WebView console is forwarded to the host as `dev_log`, so these lines land in the
6
+ * Mentra App log buffer next to the host's and ship with bug reports.
7
+ *
8
+ * Nothing here runs per frame. Per-frame facts are counters.
9
+ */
10
+
11
+ /** Grep marker. Never build this by concatenation — a single grep must be exhaustive. */
12
+ export const PREVIEW_TRACE_MARKER = "PREVIEW_TRACE"
13
+
14
+ const REDACTED = "<redacted>"
15
+ const SENSITIVE_KEYS = ["token", "secret", "password", "authorization", "url", "meetingurl"]
16
+ /** Repeated identical warnings: log the first, then a count at most this often. */
17
+ const RATE_LIMIT_WINDOW_MS = 10_000
18
+
19
+ export type PreviewTraceFields = Record<string, string | number | boolean | null | undefined>
20
+
21
+ type Sink = (line: string, level: "log" | "warn") => void
22
+
23
+ function defaultSink(line: string, level: "log" | "warn"): void {
24
+ if (typeof console === "undefined") return
25
+ if (level === "warn") console.warn(line)
26
+ else console.log(line)
27
+ }
28
+
29
+ let sink: Sink = defaultSink
30
+ let now: () => number = () => (typeof performance !== "undefined" ? performance.now() : Date.now())
31
+
32
+ function isSensitive(key: string): boolean {
33
+ const lower = key.toLowerCase()
34
+ return SENSITIVE_KEYS.some((sensitive) => lower.includes(sensitive))
35
+ }
36
+
37
+ /** Render one line. Exported for tests. */
38
+ export function formatPreviewTrace(phase: string, fields: PreviewTraceFields = {}): string {
39
+ const parts = [`[${PREVIEW_TRACE_MARKER}]`, `layer=page`, `phase=${phase}`]
40
+ for (const [key, value] of Object.entries(fields)) {
41
+ if (value === undefined) continue
42
+ const rendered = isSensitive(key) ? REDACTED : String(value).replace(/\s+/g, "_")
43
+ parts.push(`${key}=${rendered}`)
44
+ }
45
+ parts.push(`t=${Math.round(now())}`)
46
+ return parts.join(" ")
47
+ }
48
+
49
+ export function previewTrace(phase: string, fields?: PreviewTraceFields): void {
50
+ sink(formatPreviewTrace(phase, fields), "log")
51
+ }
52
+
53
+ export function previewTraceWarn(phase: string, fields?: PreviewTraceFields): void {
54
+ sink(formatPreviewTrace(phase, fields), "warn")
55
+ }
56
+
57
+ const rateLimited = new Map<string, {firstAt: number; suppressed: number}>()
58
+
59
+ /** Warn once per key, then report how many were suppressed at most every 10 s. */
60
+ export function previewTraceWarnLimited(key: string, phase: string, fields?: PreviewTraceFields): void {
61
+ const at = now()
62
+ const entry = rateLimited.get(key)
63
+ if (!entry) {
64
+ rateLimited.set(key, {firstAt: at, suppressed: 0})
65
+ previewTraceWarn(phase, fields)
66
+ return
67
+ }
68
+ entry.suppressed += 1
69
+ if (at - entry.firstAt >= RATE_LIMIT_WINDOW_MS) {
70
+ previewTraceWarn(phase, {...fields, repeated: entry.suppressed})
71
+ entry.firstAt = at
72
+ entry.suppressed = 0
73
+ }
74
+ }
75
+
76
+ /** Test seam: capture lines and drive the clock. Pass nothing to restore the defaults. */
77
+ export function setPreviewTraceSinkForTests(next?: Sink, clock?: () => number): void {
78
+ sink = next ?? defaultSink
79
+ now = clock ?? (() => (typeof performance !== "undefined" ? performance.now() : Date.now()))
80
+ rateLimited.clear()
81
+ }
@@ -0,0 +1,242 @@
1
+ /**
2
+ * The two ways raw frames reach this WebView, behind one interface.
3
+ *
4
+ * iOS has no way to hand a WebView binary without a copy through JavaScriptCore, so it gets a
5
+ * loopback WebSocket. Android has `WebViewCompat.addWebMessageListener`, which delivers an
6
+ * ArrayBuffer to a JS object the native side installs on `window`. Which one is in use is decided
7
+ * by the host's config, never by the page.
8
+ *
9
+ * Reconnection is not here. Every failure is surfaced with its own reason and `PreviewConnection`
10
+ * decides whether to handshake again.
11
+ */
12
+
13
+ import {previewTrace, previewTraceWarn} from "./trace"
14
+
15
+ export interface PreviewTransport {
16
+ connect(): Promise<void>
17
+ onBinary(cb: (buf: ArrayBuffer) => void): void
18
+ onError(cb: (reason: string) => void): void
19
+ sendText(json: string): void
20
+ close(): void
21
+ }
22
+
23
+ /** Carries the machine-readable reason so a caller does not have to match on message text. */
24
+ export class PreviewTransportError extends Error {
25
+ readonly reason: string
26
+
27
+ constructor(reason: string) {
28
+ super(reason)
29
+ this.name = "PreviewTransportError"
30
+ this.reason = reason
31
+ }
32
+ }
33
+
34
+ /** The Android-side object installed by `WebViewCompat.addWebMessageListener`. */
35
+ export interface PreviewWebMessagePort {
36
+ postMessage(message: string): void
37
+ addEventListener(type: "message", handler: (event: {data: unknown}) => void): void
38
+ removeEventListener?(type: "message", handler: (event: {data: unknown}) => void): void
39
+ }
40
+
41
+ /** Used when an older host config omits `portName`. */
42
+ export const DEFAULT_PREVIEW_PORT_NAME = "MentraFramePreviewPort"
43
+
44
+ /** `{t:"ack",gen,seq}` — the one message the consumer sends per frame. */
45
+ export function ackMessage(gen: number, seq: number): string {
46
+ return JSON.stringify({t: "ack", gen, seq})
47
+ }
48
+
49
+ function helloMessage(token: string): string {
50
+ return JSON.stringify({t: "hello", token})
51
+ }
52
+
53
+ /** iOS: a loopback WebSocket from the native host. */
54
+ export class WebSocketTransport implements PreviewTransport {
55
+ private socket: WebSocket | null = null
56
+ private binaryHandler: ((buf: ArrayBuffer) => void) | null = null
57
+ private errorHandler: ((reason: string) => void) | null = null
58
+ private opened = false
59
+ private closed = false
60
+
61
+ constructor(
62
+ private readonly url: string,
63
+ private readonly token: string,
64
+ ) {}
65
+
66
+ connect(): Promise<void> {
67
+ return new Promise((resolve, reject) => {
68
+ let socket: WebSocket
69
+ try {
70
+ socket = new WebSocket(this.url)
71
+ } catch (error) {
72
+ previewTraceWarn("transport_error", {
73
+ transport: "websocket",
74
+ reason: "websocket_unavailable",
75
+ error: error instanceof Error ? error.message : String(error),
76
+ })
77
+ reject(new PreviewTransportError("websocket_unavailable"))
78
+ return
79
+ }
80
+ socket.binaryType = "arraybuffer"
81
+ this.socket = socket
82
+ socket.onopen = () => {
83
+ this.opened = true
84
+ try {
85
+ // `connect()` must not resolve before the host knows who we are, or the first frames
86
+ // would race the hello and be dropped by the sender as unauthenticated.
87
+ socket.send(helloMessage(this.token))
88
+ } catch {
89
+ this.fail("hello_send_failed")
90
+ reject(new PreviewTransportError("hello_send_failed"))
91
+ return
92
+ }
93
+ previewTrace("transport_open", {transport: "websocket"})
94
+ resolve()
95
+ }
96
+ socket.onmessage = (event: MessageEvent) => {
97
+ // Text frames are not this channel: control lives on the `_preview` UI channel.
98
+ if (event.data instanceof ArrayBuffer) this.binaryHandler?.(event.data)
99
+ }
100
+ socket.onerror = () => {
101
+ const reason = this.opened ? "websocket_error" : "websocket_connect_error"
102
+ this.fail(reason)
103
+ if (!this.opened) reject(new PreviewTransportError(reason))
104
+ }
105
+ socket.onclose = (event: CloseEvent) => {
106
+ const reason = this.opened ? `websocket_closed:${event.code}` : `websocket_closed_before_open:${event.code}`
107
+ this.fail(reason)
108
+ if (!this.opened) reject(new PreviewTransportError(reason))
109
+ }
110
+ })
111
+ }
112
+
113
+ onBinary(cb: (buf: ArrayBuffer) => void): void {
114
+ this.binaryHandler = cb
115
+ }
116
+
117
+ onError(cb: (reason: string) => void): void {
118
+ this.errorHandler = cb
119
+ }
120
+
121
+ sendText(json: string): void {
122
+ const socket = this.socket
123
+ if (!socket || socket.readyState !== WebSocket.OPEN) {
124
+ this.fail("send_while_closed")
125
+ return
126
+ }
127
+ try {
128
+ socket.send(json)
129
+ } catch {
130
+ this.fail("send_failed")
131
+ }
132
+ }
133
+
134
+ close(): void {
135
+ this.closed = true
136
+ const socket = this.socket
137
+ this.socket = null
138
+ if (!socket) return
139
+ socket.onopen = null
140
+ socket.onmessage = null
141
+ socket.onerror = null
142
+ socket.onclose = null
143
+ try {
144
+ socket.close()
145
+ } catch {
146
+ // A socket that never opened throws here on some engines; the transport is gone either way.
147
+ }
148
+ }
149
+
150
+ private fail(reason: string): void {
151
+ // A close the caller asked for is not a failure.
152
+ if (this.closed) return
153
+ this.errorHandler?.(reason)
154
+ }
155
+ }
156
+
157
+ /** Android: the JS object native installs with `WebViewCompat.addWebMessageListener`. */
158
+ export class WebMessagePortTransport implements PreviewTransport {
159
+ private port: PreviewWebMessagePort | null = null
160
+ private binaryHandler: ((buf: ArrayBuffer) => void) | null = null
161
+ private errorHandler: ((reason: string) => void) | null = null
162
+ private closed = false
163
+ private readonly listener = (event: {data: unknown}) => {
164
+ if (event.data instanceof ArrayBuffer) {
165
+ this.binaryHandler?.(event.data)
166
+ return
167
+ }
168
+ if (typeof event.data !== "string") this.report("port_unexpected_payload")
169
+ }
170
+
171
+ constructor(
172
+ private readonly token: string,
173
+ private readonly portName: string = DEFAULT_PREVIEW_PORT_NAME,
174
+ ) {}
175
+
176
+ connect(): Promise<void> {
177
+ const port =
178
+ typeof window === "undefined"
179
+ ? undefined
180
+ : ((window as unknown as Record<string, unknown>)[this.portName] as PreviewWebMessagePort | undefined)
181
+ if (!port) return Promise.reject(new PreviewTransportError("port_missing"))
182
+ this.port = port
183
+ port.addEventListener("message", this.listener)
184
+ try {
185
+ port.postMessage(helloMessage(this.token))
186
+ } catch {
187
+ return Promise.reject(new PreviewTransportError("hello_send_failed"))
188
+ }
189
+ previewTrace("transport_open", {transport: "webmessage"})
190
+ return Promise.resolve()
191
+ }
192
+
193
+ onBinary(cb: (buf: ArrayBuffer) => void): void {
194
+ this.binaryHandler = cb
195
+ }
196
+
197
+ onError(cb: (reason: string) => void): void {
198
+ this.errorHandler = cb
199
+ }
200
+
201
+ sendText(json: string): void {
202
+ const port = this.port
203
+ if (!port) {
204
+ this.report("send_while_closed")
205
+ return
206
+ }
207
+ try {
208
+ port.postMessage(json)
209
+ } catch {
210
+ this.report("send_failed")
211
+ }
212
+ }
213
+
214
+ close(): void {
215
+ this.closed = true
216
+ this.port?.removeEventListener?.("message", this.listener)
217
+ this.port = null
218
+ }
219
+
220
+ private report(reason: string): void {
221
+ if (this.closed) return
222
+ this.errorHandler?.(reason)
223
+ }
224
+ }
225
+
226
+ export interface PreviewTransportOptions {
227
+ url?: string
228
+ portName?: string
229
+ token: string
230
+ }
231
+
232
+ /** Build the transport the host asked for. The page never picks on its own. */
233
+ export function createPreviewTransport(
234
+ transport: "websocket" | "webmessage",
235
+ options: PreviewTransportOptions,
236
+ ): PreviewTransport {
237
+ if (transport === "websocket") {
238
+ if (!options.url) throw new PreviewTransportError("missing_url")
239
+ return new WebSocketTransport(options.url, options.token)
240
+ }
241
+ return new WebMessagePortTransport(options.token, options.portName)
242
+ }