@mentra/miniapp 3.2.1-beta.315 → 3.2.1-beta.354

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,187 @@
1
+ /**
2
+ * `<StreamPreview>` — draws the raw decoded frames of the source the miniapp's background has
3
+ * leased with `session.stream.preview()`.
4
+ *
5
+ * The component owns only the renderer (canvas, WebGL context, textures). Mounting it shows the
6
+ * preview; unmounting it hides the preview and stops production, but keeps the document's
7
+ * connection open so the next mount starts without a handshake. Releasing the lease is the
8
+ * background's job (`handle.stop()`), not this component's.
9
+ *
10
+ * Frame bytes never enter React state: frames go from the transport to the renderer directly.
11
+ */
12
+
13
+ import {useEffect, useRef, type CSSProperties} from "react"
14
+
15
+ import type {PreviewConnectionState, PreviewSink} from "./stream-preview/PreviewConnection"
16
+ import {getPreviewConnection} from "./stream-preview/PreviewConnection"
17
+ import {ResizeCoalescer, type PreviewBox} from "./stream-preview/resizeCoalescer"
18
+ import {quantizePreviewTier, sameTier, type PreviewTier} from "./stream-preview/tiers"
19
+ import {previewTrace, previewTraceWarn} from "./stream-preview/trace"
20
+ import {
21
+ createYuvRenderer,
22
+ type PreviewFit,
23
+ type YuvRenderer,
24
+ type YuvRendererOptions,
25
+ } from "./stream-preview/yuvRenderer"
26
+
27
+ export type {PreviewFit} from "./stream-preview/yuvRenderer"
28
+ export type {PreviewConnectionState} from "./stream-preview/PreviewConnection"
29
+
30
+ export interface StreamPreviewStatus {
31
+ state: PreviewConnectionState
32
+ /** The Mentra App is in the background; production is paused and resumes on return. */
33
+ paused: boolean
34
+ /** Why the state changed, when the host said (`source_ended`, `stopped`, ...). */
35
+ reason?: string
36
+ /** Error code for `error` / `unsupported`. */
37
+ code?: string
38
+ /** Latest once-a-second host counters plus page-side counters. */
39
+ stats?: Record<string, unknown>
40
+ }
41
+
42
+ export interface StreamPreviewProps {
43
+ /** `contain` letterboxes (default); `cover` fills and crops. The picture is never stretched. */
44
+ fit?: PreviewFit
45
+ onStatus?: (status: StreamPreviewStatus) => void
46
+ /** Typed preview error codes: `source_ended`, `unsupported`, `transport_failed`, ... */
47
+ onError?: (code: string) => void
48
+ className?: string
49
+ style?: CSSProperties
50
+ }
51
+
52
+ type RendererFactory = (canvas: HTMLCanvasElement, options: YuvRendererOptions) => YuvRenderer | null
53
+
54
+ let rendererFactory: RendererFactory = createYuvRenderer
55
+
56
+ /** Test seam: WebGL is not available in headless DOMs. */
57
+ export function setStreamPreviewRendererFactoryForTests(factory: RendererFactory | null): void {
58
+ rendererFactory = factory ?? createYuvRenderer
59
+ }
60
+
61
+ const DEFAULT_STYLE: CSSProperties = {display: "block", width: "100%", height: "100%"}
62
+
63
+ function measure(canvas: HTMLCanvasElement): PreviewBox {
64
+ const rect = canvas.getBoundingClientRect()
65
+ const ratio = typeof window === "undefined" ? 1 : window.devicePixelRatio || 1
66
+ return {width: Math.round(rect.width * ratio), height: Math.round(rect.height * ratio)}
67
+ }
68
+
69
+ function isDocumentVisible(): boolean {
70
+ return typeof document === "undefined" || document.visibilityState !== "hidden"
71
+ }
72
+
73
+ export function StreamPreview({fit = "contain", onStatus, onError, className, style}: StreamPreviewProps) {
74
+ const canvasRef = useRef<HTMLCanvasElement | null>(null)
75
+ const rendererRef = useRef<YuvRenderer | null>(null)
76
+ const fitRef = useRef(fit)
77
+ const onStatusRef = useRef(onStatus)
78
+ const onErrorRef = useRef(onError)
79
+ fitRef.current = fit
80
+ onStatusRef.current = onStatus
81
+ onErrorRef.current = onError
82
+
83
+ useEffect(() => {
84
+ rendererRef.current?.setFit(fit)
85
+ }, [fit])
86
+
87
+ useEffect(() => {
88
+ const canvas = canvasRef.current
89
+ if (!canvas) return
90
+ const connection = getPreviewConnection()
91
+ const mountEpoch = connection.nextMountEpoch()
92
+ let contextLosses = 0
93
+ let status: StreamPreviewStatus = {state: connection.currentState, paused: false}
94
+ let box: PreviewBox = {width: 0, height: 0}
95
+ let tier: PreviewTier | null = null
96
+ let running = isDocumentVisible()
97
+ let unmounted = false
98
+
99
+ const emit = (next: Partial<StreamPreviewStatus>) => {
100
+ status = {...status, ...next}
101
+ onStatusRef.current?.(status)
102
+ }
103
+
104
+ const buildRenderer = () => {
105
+ rendererRef.current?.dispose()
106
+ rendererRef.current = rendererFactory(canvas, {
107
+ fit: fitRef.current,
108
+ onError: (reason) => {
109
+ previewTraceWarn("renderer_error", {reason, mountEpoch, docGen: connection.documentGeneration})
110
+ connection.reportRendererError(mountEpoch, reason)
111
+ if (reason === "webgl_context_lost") contextLosses += 1
112
+ },
113
+ })
114
+ if (!rendererRef.current) connection.reportRendererError(mountEpoch, "webgl_unavailable")
115
+ }
116
+ buildRenderer()
117
+
118
+ // Context loss rebuilds only the renderer. The connection and the lease are not involved,
119
+ // and frames that arrive meanwhile are still acked so the sender does not stall.
120
+ const onContextRestored = () => {
121
+ if (unmounted) return
122
+ previewTrace("renderer_rebuilt", {mountEpoch, contextLosses})
123
+ buildRenderer()
124
+ }
125
+ canvas.addEventListener("webglcontextrestored", onContextRestored)
126
+
127
+ const sink: PreviewSink = {
128
+ mountEpoch,
129
+ draw: (frame) => rendererRef.current?.drawFrame(frame) ?? null,
130
+ onState: (state, detail) =>
131
+ emit({state, paused: detail.paused ?? false, reason: detail.reason, code: detail.code}),
132
+ onStatus: (stats) => emit({stats: {...stats, contextLosses}}),
133
+ onError: (code) => onErrorRef.current?.(code),
134
+ }
135
+ const detach = connection.attach(sink)
136
+ previewTrace("mount", {mountEpoch, docGen: connection.documentGeneration})
137
+
138
+ const push = () => connection.update(mountEpoch, {boxWidth: box.width, boxHeight: box.height, tier, running})
139
+
140
+ const coalescer = new ResizeCoalescer({
141
+ apply: (next) => {
142
+ box = next
143
+ const nextTier = quantizePreviewTier(next.width, next.height)
144
+ if (sameTier(nextTier, tier) && tier !== null) return
145
+ tier = nextTier
146
+ if (!tier) previewTrace("hidden_zero_size", {mountEpoch})
147
+ push()
148
+ },
149
+ })
150
+
151
+ let resizeObserver: ResizeObserver | null = null
152
+ const onWindowResize = () => coalescer.push(measure(canvas))
153
+ if (typeof ResizeObserver === "function") {
154
+ resizeObserver = new ResizeObserver(() => coalescer.push(measure(canvas)))
155
+ resizeObserver.observe(canvas)
156
+ } else if (typeof window !== "undefined") {
157
+ window.addEventListener("resize", onWindowResize)
158
+ }
159
+ coalescer.push(measure(canvas))
160
+
161
+ const onVisibility = () => {
162
+ const visible = isDocumentVisible()
163
+ if (visible === running) return
164
+ running = visible
165
+ previewTrace("visibility_change", {mountEpoch, visible})
166
+ push()
167
+ }
168
+ document.addEventListener("visibilitychange", onVisibility)
169
+
170
+ return () => {
171
+ unmounted = true
172
+ previewTrace("unmount", {mountEpoch, docGen: connection.documentGeneration})
173
+ document.removeEventListener("visibilitychange", onVisibility)
174
+ resizeObserver?.disconnect()
175
+ if (typeof window !== "undefined") window.removeEventListener("resize", onWindowResize)
176
+ coalescer.cancel()
177
+ canvas.removeEventListener("webglcontextrestored", onContextRestored)
178
+ running = false
179
+ push()
180
+ detach()
181
+ rendererRef.current?.dispose()
182
+ rendererRef.current = null
183
+ }
184
+ }, [])
185
+
186
+ return <canvas ref={canvasRef} className={className} style={{...DEFAULT_STYLE, ...style}} />
187
+ }
@@ -10,3 +10,5 @@ export type {MiniappHeaderProps} from "./MiniappHeader"
10
10
  export {MentraProvider} from "./MentraProvider"
11
11
  export type {MentraProviderProps} from "./MentraProvider"
12
12
  export {useRpc, type RpcCallable} from "./useRpc"
13
+ export {StreamPreview} from "./StreamPreview"
14
+ export type {StreamPreviewProps, StreamPreviewStatus, PreviewFit, PreviewConnectionState} from "./StreamPreview"