@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.
- package/README.md +16 -0
- package/dist/background/index.d.ts +5 -3
- package/dist/background/index.d.ts.map +1 -1
- package/dist/background/index.js +1 -0
- package/dist/background/index.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/modules/display.d.ts +33 -0
- package/dist/modules/display.d.ts.map +1 -1
- package/dist/modules/display.js +2 -0
- package/dist/modules/display.js.map +1 -1
- package/dist/modules/meeting.d.ts +66 -6
- package/dist/modules/meeting.d.ts.map +1 -1
- package/dist/modules/meeting.js +72 -19
- package/dist/modules/meeting.js.map +1 -1
- package/dist/modules/stream.d.ts +46 -0
- package/dist/modules/stream.d.ts.map +1 -1
- package/dist/modules/stream.js +107 -0
- package/dist/modules/stream.js.map +1 -1
- package/dist/protocol.d.ts +39 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +36 -0
- package/dist/protocol.js.map +1 -1
- package/dist/react/StreamPreview.d.ts +41 -0
- package/dist/react/StreamPreview.d.ts.map +1 -0
- package/dist/react/StreamPreview.js +144 -0
- package/dist/react/StreamPreview.js.map +1 -0
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +1 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/stream-preview/PreviewConnection.d.ts +139 -0
- package/dist/react/stream-preview/PreviewConnection.d.ts.map +1 -0
- package/dist/react/stream-preview/PreviewConnection.js +462 -0
- package/dist/react/stream-preview/PreviewConnection.js.map +1 -0
- package/dist/react/stream-preview/controlChannel.d.ts +75 -0
- package/dist/react/stream-preview/controlChannel.d.ts.map +1 -0
- package/dist/react/stream-preview/controlChannel.js +55 -0
- package/dist/react/stream-preview/controlChannel.js.map +1 -0
- package/dist/react/stream-preview/creditLoop.d.ts +81 -0
- package/dist/react/stream-preview/creditLoop.d.ts.map +1 -0
- package/dist/react/stream-preview/creditLoop.js +146 -0
- package/dist/react/stream-preview/creditLoop.js.map +1 -0
- package/dist/react/stream-preview/protocol.d.ts +113 -0
- package/dist/react/stream-preview/protocol.d.ts.map +1 -0
- package/dist/react/stream-preview/protocol.js +148 -0
- package/dist/react/stream-preview/protocol.js.map +1 -0
- package/dist/react/stream-preview/resizeCoalescer.d.ts +38 -0
- package/dist/react/stream-preview/resizeCoalescer.d.ts.map +1 -0
- package/dist/react/stream-preview/resizeCoalescer.js +71 -0
- package/dist/react/stream-preview/resizeCoalescer.js.map +1 -0
- package/dist/react/stream-preview/testFrames.d.ts +38 -0
- package/dist/react/stream-preview/testFrames.d.ts.map +1 -0
- package/dist/react/stream-preview/testFrames.js +50 -0
- package/dist/react/stream-preview/testFrames.js.map +1 -0
- package/dist/react/stream-preview/tiers.d.ts +21 -0
- package/dist/react/stream-preview/tiers.d.ts.map +1 -0
- package/dist/react/stream-preview/tiers.js +33 -0
- package/dist/react/stream-preview/tiers.js.map +1 -0
- package/dist/react/stream-preview/trace.d.ts +23 -0
- package/dist/react/stream-preview/trace.d.ts.map +1 -0
- package/dist/react/stream-preview/trace.js +71 -0
- package/dist/react/stream-preview/trace.js.map +1 -0
- package/dist/react/stream-preview/transport.d.ts +79 -0
- package/dist/react/stream-preview/transport.d.ts.map +1 -0
- package/dist/react/stream-preview/transport.js +209 -0
- package/dist/react/stream-preview/transport.js.map +1 -0
- package/dist/react/stream-preview/yuvRenderer.d.ts +31 -0
- package/dist/react/stream-preview/yuvRenderer.d.ts.map +1 -0
- package/dist/react/stream-preview/yuvRenderer.js +398 -0
- package/dist/react/stream-preview/yuvRenderer.js.map +1 -0
- package/dist/session.d.ts +5 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +5 -0
- package/dist/session.js.map +1 -1
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +1 -0
- package/dist/ui/index.js.map +1 -1
- package/package.json +8 -3
- package/src/background/index.ts +14 -1
- package/src/index.ts +15 -0
- package/src/modules/display.ts +37 -0
- package/src/modules/meeting.ts +133 -23
- package/src/modules/stream.ts +153 -1
- package/src/protocol.ts +75 -0
- package/src/react/StreamPreview.tsx +187 -0
- package/src/react/index.ts +2 -0
- package/src/react/stream-preview/PreviewConnection.ts +552 -0
- package/src/react/stream-preview/controlChannel.ts +119 -0
- package/src/react/stream-preview/creditLoop.ts +189 -0
- package/src/react/stream-preview/protocol.ts +227 -0
- package/src/react/stream-preview/resizeCoalescer.ts +88 -0
- package/src/react/stream-preview/testFrames.ts +86 -0
- package/src/react/stream-preview/tiers.ts +37 -0
- package/src/react/stream-preview/trace.ts +81 -0
- package/src/react/stream-preview/transport.ts +242 -0
- package/src/react/stream-preview/yuvRenderer.ts +453 -0
- package/src/session.ts +8 -0
- 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
|
+
}
|