@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,209 @@
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
+ import { previewTrace, previewTraceWarn } from "./trace";
13
+ /** Carries the machine-readable reason so a caller does not have to match on message text. */
14
+ export class PreviewTransportError extends Error {
15
+ constructor(reason) {
16
+ super(reason);
17
+ this.name = "PreviewTransportError";
18
+ this.reason = reason;
19
+ }
20
+ }
21
+ /** Used when an older host config omits `portName`. */
22
+ export const DEFAULT_PREVIEW_PORT_NAME = "MentraFramePreviewPort";
23
+ /** `{t:"ack",gen,seq}` — the one message the consumer sends per frame. */
24
+ export function ackMessage(gen, seq) {
25
+ return JSON.stringify({ t: "ack", gen, seq });
26
+ }
27
+ function helloMessage(token) {
28
+ return JSON.stringify({ t: "hello", token });
29
+ }
30
+ /** iOS: a loopback WebSocket from the native host. */
31
+ export class WebSocketTransport {
32
+ constructor(url, token) {
33
+ this.url = url;
34
+ this.token = token;
35
+ this.socket = null;
36
+ this.binaryHandler = null;
37
+ this.errorHandler = null;
38
+ this.opened = false;
39
+ this.closed = false;
40
+ }
41
+ connect() {
42
+ return new Promise((resolve, reject) => {
43
+ let socket;
44
+ try {
45
+ socket = new WebSocket(this.url);
46
+ }
47
+ catch (error) {
48
+ previewTraceWarn("transport_error", {
49
+ transport: "websocket",
50
+ reason: "websocket_unavailable",
51
+ error: error instanceof Error ? error.message : String(error),
52
+ });
53
+ reject(new PreviewTransportError("websocket_unavailable"));
54
+ return;
55
+ }
56
+ socket.binaryType = "arraybuffer";
57
+ this.socket = socket;
58
+ socket.onopen = () => {
59
+ this.opened = true;
60
+ try {
61
+ // `connect()` must not resolve before the host knows who we are, or the first frames
62
+ // would race the hello and be dropped by the sender as unauthenticated.
63
+ socket.send(helloMessage(this.token));
64
+ }
65
+ catch {
66
+ this.fail("hello_send_failed");
67
+ reject(new PreviewTransportError("hello_send_failed"));
68
+ return;
69
+ }
70
+ previewTrace("transport_open", { transport: "websocket" });
71
+ resolve();
72
+ };
73
+ socket.onmessage = (event) => {
74
+ // Text frames are not this channel: control lives on the `_preview` UI channel.
75
+ if (event.data instanceof ArrayBuffer)
76
+ this.binaryHandler?.(event.data);
77
+ };
78
+ socket.onerror = () => {
79
+ const reason = this.opened ? "websocket_error" : "websocket_connect_error";
80
+ this.fail(reason);
81
+ if (!this.opened)
82
+ reject(new PreviewTransportError(reason));
83
+ };
84
+ socket.onclose = (event) => {
85
+ const reason = this.opened ? `websocket_closed:${event.code}` : `websocket_closed_before_open:${event.code}`;
86
+ this.fail(reason);
87
+ if (!this.opened)
88
+ reject(new PreviewTransportError(reason));
89
+ };
90
+ });
91
+ }
92
+ onBinary(cb) {
93
+ this.binaryHandler = cb;
94
+ }
95
+ onError(cb) {
96
+ this.errorHandler = cb;
97
+ }
98
+ sendText(json) {
99
+ const socket = this.socket;
100
+ if (!socket || socket.readyState !== WebSocket.OPEN) {
101
+ this.fail("send_while_closed");
102
+ return;
103
+ }
104
+ try {
105
+ socket.send(json);
106
+ }
107
+ catch {
108
+ this.fail("send_failed");
109
+ }
110
+ }
111
+ close() {
112
+ this.closed = true;
113
+ const socket = this.socket;
114
+ this.socket = null;
115
+ if (!socket)
116
+ return;
117
+ socket.onopen = null;
118
+ socket.onmessage = null;
119
+ socket.onerror = null;
120
+ socket.onclose = null;
121
+ try {
122
+ socket.close();
123
+ }
124
+ catch {
125
+ // A socket that never opened throws here on some engines; the transport is gone either way.
126
+ }
127
+ }
128
+ fail(reason) {
129
+ // A close the caller asked for is not a failure.
130
+ if (this.closed)
131
+ return;
132
+ this.errorHandler?.(reason);
133
+ }
134
+ }
135
+ /** Android: the JS object native installs with `WebViewCompat.addWebMessageListener`. */
136
+ export class WebMessagePortTransport {
137
+ constructor(token, portName = DEFAULT_PREVIEW_PORT_NAME) {
138
+ this.token = token;
139
+ this.portName = portName;
140
+ this.port = null;
141
+ this.binaryHandler = null;
142
+ this.errorHandler = null;
143
+ this.closed = false;
144
+ this.listener = (event) => {
145
+ if (event.data instanceof ArrayBuffer) {
146
+ this.binaryHandler?.(event.data);
147
+ return;
148
+ }
149
+ if (typeof event.data !== "string")
150
+ this.report("port_unexpected_payload");
151
+ };
152
+ }
153
+ connect() {
154
+ const port = typeof window === "undefined"
155
+ ? undefined
156
+ : window[this.portName];
157
+ if (!port)
158
+ return Promise.reject(new PreviewTransportError("port_missing"));
159
+ this.port = port;
160
+ port.addEventListener("message", this.listener);
161
+ try {
162
+ port.postMessage(helloMessage(this.token));
163
+ }
164
+ catch {
165
+ return Promise.reject(new PreviewTransportError("hello_send_failed"));
166
+ }
167
+ previewTrace("transport_open", { transport: "webmessage" });
168
+ return Promise.resolve();
169
+ }
170
+ onBinary(cb) {
171
+ this.binaryHandler = cb;
172
+ }
173
+ onError(cb) {
174
+ this.errorHandler = cb;
175
+ }
176
+ sendText(json) {
177
+ const port = this.port;
178
+ if (!port) {
179
+ this.report("send_while_closed");
180
+ return;
181
+ }
182
+ try {
183
+ port.postMessage(json);
184
+ }
185
+ catch {
186
+ this.report("send_failed");
187
+ }
188
+ }
189
+ close() {
190
+ this.closed = true;
191
+ this.port?.removeEventListener?.("message", this.listener);
192
+ this.port = null;
193
+ }
194
+ report(reason) {
195
+ if (this.closed)
196
+ return;
197
+ this.errorHandler?.(reason);
198
+ }
199
+ }
200
+ /** Build the transport the host asked for. The page never picks on its own. */
201
+ export function createPreviewTransport(transport, options) {
202
+ if (transport === "websocket") {
203
+ if (!options.url)
204
+ throw new PreviewTransportError("missing_url");
205
+ return new WebSocketTransport(options.url, options.token);
206
+ }
207
+ return new WebMessagePortTransport(options.token, options.portName);
208
+ }
209
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../../../src/react/stream-preview/transport.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * WebGL1 YUV → RGB renderer for raw preview frames.
3
+ *
4
+ * WebGL1 only, on purpose: the Mentra App supports iOS 15.5 / Safari 15, where WebGL2, WebCodecs,
5
+ * `VideoFrame` and `OffscreenCanvas` are either absent or unreliable. Everything here is the
6
+ * 2013 feature set — `LUMINANCE` textures, one draw call, a fragment shader doing the colour
7
+ * conversion.
8
+ *
9
+ * Rules:
10
+ * - JavaScript never touches a pixel. Planes go to `texImage2D`/`texSubImage2D` as views.
11
+ * - `readPixels` is never called; it would stall the pipeline.
12
+ * - `drawFrame` times the SUBMIT, not the paint. See the comment on that method.
13
+ */
14
+ import type { ParsedFrame } from "./protocol";
15
+ /** `contain` letterboxes; `cover` fills the canvas and crops the overflow. Never stretches. */
16
+ export type PreviewFit = "contain" | "cover";
17
+ export interface YuvRendererOptions {
18
+ /** Context loss, a failed compile, a failed upload: anything that makes the canvas stop. */
19
+ onError?: (reason: string) => void;
20
+ fit?: PreviewFit;
21
+ }
22
+ export interface YuvRenderer {
23
+ /** False once the GL context is lost, until it is restored. */
24
+ readonly alive: boolean;
25
+ setFit(fit: PreviewFit): void;
26
+ drawFrame(frame: ParsedFrame): number | null;
27
+ dispose(): void;
28
+ }
29
+ /** Create the renderer, or null when this WebView has no WebGL1 context to give. */
30
+ export declare function createYuvRenderer(canvas: HTMLCanvasElement, options?: YuvRendererOptions): YuvRenderer | null;
31
+ //# sourceMappingURL=yuvRenderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"yuvRenderer.d.ts","sourceRoot":"","sources":["../../../src/react/stream-preview/yuvRenderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAC,WAAW,EAA4D,MAAM,YAAY,CAAA;AA6EtG,+FAA+F;AAC/F,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,OAAO,CAAA;AAE5C,MAAM,WAAW,kBAAkB;IACjC,4FAA4F;IAC5F,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC,GAAG,CAAC,EAAE,UAAU,CAAA;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,+DAA+D;IAC/D,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;IACvB,MAAM,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CAAA;IAC7B,SAAS,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,GAAG,IAAI,CAAA;IAC5C,OAAO,IAAI,IAAI,CAAA;CAChB;AAwUD,oFAAoF;AACpF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,iBAAiB,EAAE,OAAO,GAAE,kBAAuB,GAAG,WAAW,GAAG,IAAI,CAiBjH"}
@@ -0,0 +1,398 @@
1
+ /**
2
+ * WebGL1 YUV → RGB renderer for raw preview frames.
3
+ *
4
+ * WebGL1 only, on purpose: the Mentra App supports iOS 15.5 / Safari 15, where WebGL2, WebCodecs,
5
+ * `VideoFrame` and `OffscreenCanvas` are either absent or unreliable. Everything here is the
6
+ * 2013 feature set — `LUMINANCE` textures, one draw call, a fragment shader doing the colour
7
+ * conversion.
8
+ *
9
+ * Rules:
10
+ * - JavaScript never touches a pixel. Planes go to `texImage2D`/`texSubImage2D` as views.
11
+ * - `readPixels` is never called; it would stall the pipeline.
12
+ * - `drawFrame` times the SUBMIT, not the paint. See the comment on that method.
13
+ */
14
+ import { previewTrace, previewTraceWarn } from "./trace";
15
+ const VERTEX_SHADER = `
16
+ attribute vec2 aPosition;
17
+ attribute vec2 aTexCoord;
18
+ uniform mat2 uTransform;
19
+ varying vec2 vTexCoord;
20
+ void main() {
21
+ gl_Position = vec4(uTransform * aPosition, 0.0, 1.0);
22
+ vTexCoord = aTexCoord;
23
+ }
24
+ `;
25
+ /**
26
+ * `uCoeff` is (Cr→R, Cb→G, Cr→G, Cb→B) for the frame's matrix; `uYOffset`/`uYScale`/`uCScale`
27
+ * carry the range. Both are uniforms rather than shader variants so a stream that switches
28
+ * colour metadata mid-run does not force a recompile.
29
+ */
30
+ function fragmentShader(format) {
31
+ const chroma = format === "nv12"
32
+ ? `uniform sampler2D uUv;
33
+ vec2 chromaSample() {
34
+ vec4 texel = texture2D(uUv, vTexCoord);
35
+ return vec2(texel.r, texel.a);
36
+ }`
37
+ : `uniform sampler2D uU;
38
+ uniform sampler2D uV;
39
+ vec2 chromaSample() {
40
+ return vec2(texture2D(uU, vTexCoord).r, texture2D(uV, vTexCoord).r);
41
+ }`;
42
+ return `
43
+ #ifdef GL_FRAGMENT_PRECISION_HIGH
44
+ precision highp float;
45
+ #else
46
+ precision mediump float;
47
+ #endif
48
+ varying vec2 vTexCoord;
49
+ uniform sampler2D uY;
50
+ uniform float uYOffset;
51
+ uniform float uYScale;
52
+ uniform float uCScale;
53
+ uniform vec4 uCoeff;
54
+ ${chroma}
55
+ void main() {
56
+ float y = (texture2D(uY, vTexCoord).r - uYOffset) * uYScale;
57
+ vec2 c = (chromaSample() - 0.5019608) * uCScale;
58
+ vec3 rgb = vec3(
59
+ y + uCoeff.x * c.y,
60
+ y + uCoeff.y * c.x + uCoeff.z * c.y,
61
+ y + uCoeff.w * c.x);
62
+ gl_FragColor = vec4(clamp(rgb, 0.0, 1.0), 1.0);
63
+ }
64
+ `;
65
+ }
66
+ /** Standard BT.601 and BT.709 conversion coefficients, as (Cr→R, Cb→G, Cr→G, Cb→B). */
67
+ const MATRIX_COEFFICIENTS = {
68
+ bt601: [1.402, -0.344136, -0.714136, 1.772],
69
+ bt709: [1.5748, -0.187324, -0.468124, 1.8556],
70
+ };
71
+ /** Limited range is Y 16..235 and chroma 16..240; full range uses the whole byte. */
72
+ const RANGE_SCALES = {
73
+ limited: { yOffset: 16 / 255, yScale: 255 / 219, cScale: 255 / 224 },
74
+ full: { yOffset: 0, yScale: 1, cScale: 1 },
75
+ };
76
+ /** Clockwise quarter turns, as exact cos/sin so 90° stays 90°. */
77
+ const ROTATIONS = [
78
+ { cos: 1, sin: 0 },
79
+ { cos: 0, sin: 1 },
80
+ { cos: -1, sin: 0 },
81
+ { cos: 0, sin: -1 },
82
+ ];
83
+ const POSITIONS = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);
84
+ // v = 0 is the first uploaded row, which is the top of the image, so it maps to NDC y = +1.
85
+ const TEX_COORDS = new Float32Array([0, 1, 1, 1, 0, 0, 1, 0]);
86
+ class WebGlYuvRenderer {
87
+ constructor(canvas, gl, options) {
88
+ this.canvas = canvas;
89
+ this.gl = gl;
90
+ this.options = options;
91
+ this.programs = new Map();
92
+ this.textures = new Map();
93
+ this.positionBuffer = null;
94
+ this.texCoordBuffer = null;
95
+ this.layout = null;
96
+ this.lost = false;
97
+ this.onContextLost = (event) => {
98
+ // Without preventDefault the context is never restored, and the canvas would stay black for
99
+ // the rest of the document.
100
+ event.preventDefault();
101
+ this.lost = true;
102
+ this.programs.clear();
103
+ this.textures.clear();
104
+ this.positionBuffer = null;
105
+ this.texCoordBuffer = null;
106
+ this.layout = null;
107
+ previewTraceWarn("webgl_context_lost");
108
+ this.options.onError?.("webgl_context_lost");
109
+ };
110
+ this.onContextRestored = () => {
111
+ // Everything was invalidated by the loss; the next frame rebuilds it lazily.
112
+ this.lost = false;
113
+ previewTrace("webgl_context_restored");
114
+ };
115
+ this.fit = options.fit ?? "contain";
116
+ canvas.addEventListener("webglcontextlost", this.onContextLost);
117
+ canvas.addEventListener("webglcontextrestored", this.onContextRestored);
118
+ }
119
+ get alive() {
120
+ return !this.lost && !this.gl.isContextLost();
121
+ }
122
+ setFit(fit) {
123
+ this.fit = fit;
124
+ }
125
+ /**
126
+ * Upload the planes and submit one draw.
127
+ *
128
+ * The returned number is how long the SUBMIT took: uploads plus `drawArrays`, measured with
129
+ * `performance.now()`. It is NOT presentation latency and not a frame time — GL is asynchronous
130
+ * and the pixels reach the screen some unknown time later. Proving presentation would need
131
+ * `readPixels` or a fence, and both change what they measure.
132
+ *
133
+ * Returns null when there is no usable context.
134
+ */
135
+ drawFrame(frame) {
136
+ if (!this.alive)
137
+ return null;
138
+ const gl = this.gl;
139
+ const startedAt = performance.now();
140
+ const bundle = this.programFor(frame.pixelFormat);
141
+ if (!bundle)
142
+ return null;
143
+ this.syncDrawingBuffer();
144
+ gl.useProgram(bundle.program);
145
+ // Tight packing: every plane row starts on a byte boundary, which the default alignment of 4
146
+ // would get wrong for any odd width.
147
+ gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
148
+ const reallocate = !this.layout ||
149
+ this.layout.width !== frame.width ||
150
+ this.layout.height !== frame.height ||
151
+ this.layout.format !== frame.pixelFormat;
152
+ if (reallocate && this.layout && this.layout.format !== frame.pixelFormat)
153
+ this.dropChromaTextures();
154
+ if (frame.pixelFormat === "nv12") {
155
+ if (!this.upload("y", 0, gl.LUMINANCE, frame.width, frame.height, frame.y, reallocate))
156
+ return null;
157
+ if (!this.upload("uv", 1, gl.LUMINANCE_ALPHA, frame.chromaWidth, frame.chromaHeight, frame.uv, reallocate)) {
158
+ return null;
159
+ }
160
+ }
161
+ else {
162
+ if (!this.upload("y", 0, gl.LUMINANCE, frame.width, frame.height, frame.y, reallocate))
163
+ return null;
164
+ if (!this.upload("u", 1, gl.LUMINANCE, frame.chromaWidth, frame.chromaHeight, frame.u, reallocate))
165
+ return null;
166
+ if (!this.upload("v", 2, gl.LUMINANCE, frame.chromaWidth, frame.chromaHeight, frame.v, reallocate))
167
+ return null;
168
+ }
169
+ if (reallocate) {
170
+ this.layout = { width: frame.width, height: frame.height, format: frame.pixelFormat };
171
+ this.bindSamplers(bundle, frame.pixelFormat);
172
+ }
173
+ const coefficients = MATRIX_COEFFICIENTS[frame.colorMatrix];
174
+ const range = RANGE_SCALES[frame.colorRange];
175
+ gl.uniform1f(bundle.uYOffset, range.yOffset);
176
+ gl.uniform1f(bundle.uYScale, range.yScale);
177
+ gl.uniform1f(bundle.uCScale, range.cScale);
178
+ gl.uniform4f(bundle.uCoeff, coefficients[0], coefficients[1], coefficients[2], coefficients[3]);
179
+ gl.uniformMatrix2fv(bundle.uTransform, false, this.transformFor(frame));
180
+ gl.clearColor(0, 0, 0, 1);
181
+ gl.clear(gl.COLOR_BUFFER_BIT);
182
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
183
+ return performance.now() - startedAt;
184
+ }
185
+ dispose() {
186
+ this.canvas.removeEventListener("webglcontextlost", this.onContextLost);
187
+ this.canvas.removeEventListener("webglcontextrestored", this.onContextRestored);
188
+ const gl = this.gl;
189
+ if (!gl.isContextLost()) {
190
+ for (const texture of this.textures.values())
191
+ gl.deleteTexture(texture);
192
+ for (const bundle of this.programs.values())
193
+ gl.deleteProgram(bundle.program);
194
+ if (this.positionBuffer)
195
+ gl.deleteBuffer(this.positionBuffer);
196
+ if (this.texCoordBuffer)
197
+ gl.deleteBuffer(this.texCoordBuffer);
198
+ }
199
+ this.textures.clear();
200
+ this.programs.clear();
201
+ this.positionBuffer = null;
202
+ this.texCoordBuffer = null;
203
+ this.layout = null;
204
+ }
205
+ /** Scale the rotated frame to contain or cover the canvas without ever stretching it. */
206
+ transformFor(frame) {
207
+ const { cos, sin } = ROTATIONS[frame.rotationQuarters] ?? ROTATIONS[0];
208
+ const quarterTurned = frame.rotationQuarters % 2 === 1;
209
+ const frameWidth = quarterTurned ? frame.height : frame.width;
210
+ const frameHeight = quarterTurned ? frame.width : frame.height;
211
+ const canvasAspect = this.canvas.width / Math.max(1, this.canvas.height);
212
+ const frameAspect = frameWidth / Math.max(1, frameHeight);
213
+ let scaleX = 1;
214
+ let scaleY = 1;
215
+ const frameIsWider = frameAspect > canvasAspect;
216
+ if (this.fit === "cover") {
217
+ if (frameIsWider)
218
+ scaleX = frameAspect / canvasAspect;
219
+ else
220
+ scaleY = canvasAspect / frameAspect;
221
+ }
222
+ else if (frameIsWider)
223
+ scaleY = canvasAspect / frameAspect;
224
+ else
225
+ scaleX = frameAspect / canvasAspect;
226
+ // Column-major for uniformMatrix2fv: scale ∘ clockwise rotation.
227
+ return new Float32Array([scaleX * cos, -scaleY * sin, scaleX * sin, scaleY * cos]);
228
+ }
229
+ syncDrawingBuffer() {
230
+ const canvas = this.canvas;
231
+ const ratio = Math.min(typeof window === "undefined" ? 1 : window.devicePixelRatio || 1, 2);
232
+ const width = Math.max(1, Math.round((canvas.clientWidth || canvas.width) * ratio));
233
+ const height = Math.max(1, Math.round((canvas.clientHeight || canvas.height) * ratio));
234
+ if (canvas.width !== width || canvas.height !== height) {
235
+ canvas.width = width;
236
+ canvas.height = height;
237
+ }
238
+ this.gl.viewport(0, 0, canvas.width, canvas.height);
239
+ }
240
+ upload(key, unit, format, width, height, pixels, reallocate) {
241
+ const gl = this.gl;
242
+ let texture = this.textures.get(key);
243
+ if (!texture) {
244
+ const created = gl.createTexture();
245
+ if (!created) {
246
+ this.options.onError?.("texture_allocation_failed");
247
+ return false;
248
+ }
249
+ texture = created;
250
+ this.textures.set(key, texture);
251
+ gl.activeTexture(gl.TEXTURE0 + unit);
252
+ gl.bindTexture(gl.TEXTURE_2D, texture);
253
+ // No mipmaps and CLAMP_TO_EDGE, which is what WebGL1 requires for non-power-of-two sizes.
254
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
255
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
256
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
257
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
258
+ gl.texImage2D(gl.TEXTURE_2D, 0, format, width, height, 0, format, gl.UNSIGNED_BYTE, pixels);
259
+ return true;
260
+ }
261
+ gl.activeTexture(gl.TEXTURE0 + unit);
262
+ gl.bindTexture(gl.TEXTURE_2D, texture);
263
+ // Reallocating every frame is the classic way to make a renderer look slower than it is.
264
+ if (reallocate)
265
+ gl.texImage2D(gl.TEXTURE_2D, 0, format, width, height, 0, format, gl.UNSIGNED_BYTE, pixels);
266
+ else
267
+ gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width, height, format, gl.UNSIGNED_BYTE, pixels);
268
+ return true;
269
+ }
270
+ /** A format switch leaves the other layout's chroma textures allocated; drop them. */
271
+ dropChromaTextures() {
272
+ for (const key of ["u", "v", "uv"]) {
273
+ const texture = this.textures.get(key);
274
+ if (!texture)
275
+ continue;
276
+ this.gl.deleteTexture(texture);
277
+ this.textures.delete(key);
278
+ }
279
+ }
280
+ bindSamplers(bundle, format) {
281
+ const gl = this.gl;
282
+ gl.uniform1i(gl.getUniformLocation(bundle.program, "uY"), 0);
283
+ if (format === "nv12") {
284
+ gl.uniform1i(gl.getUniformLocation(bundle.program, "uUv"), 1);
285
+ return;
286
+ }
287
+ gl.uniform1i(gl.getUniformLocation(bundle.program, "uU"), 1);
288
+ gl.uniform1i(gl.getUniformLocation(bundle.program, "uV"), 2);
289
+ }
290
+ programFor(format) {
291
+ const existing = this.programs.get(format);
292
+ if (existing) {
293
+ this.bindGeometry(existing);
294
+ return existing;
295
+ }
296
+ const gl = this.gl;
297
+ const program = this.link(VERTEX_SHADER, fragmentShader(format));
298
+ if (!program)
299
+ return null;
300
+ const bundle = {
301
+ program,
302
+ aPosition: gl.getAttribLocation(program, "aPosition"),
303
+ aTexCoord: gl.getAttribLocation(program, "aTexCoord"),
304
+ uTransform: gl.getUniformLocation(program, "uTransform"),
305
+ uYOffset: gl.getUniformLocation(program, "uYOffset"),
306
+ uYScale: gl.getUniformLocation(program, "uYScale"),
307
+ uCScale: gl.getUniformLocation(program, "uCScale"),
308
+ uCoeff: gl.getUniformLocation(program, "uCoeff"),
309
+ };
310
+ this.programs.set(format, bundle);
311
+ gl.useProgram(program);
312
+ this.bindSamplers(bundle, format);
313
+ this.bindGeometry(bundle);
314
+ return bundle;
315
+ }
316
+ bindGeometry(bundle) {
317
+ const gl = this.gl;
318
+ if (!this.positionBuffer) {
319
+ this.positionBuffer = gl.createBuffer();
320
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
321
+ gl.bufferData(gl.ARRAY_BUFFER, POSITIONS, gl.STATIC_DRAW);
322
+ }
323
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
324
+ gl.enableVertexAttribArray(bundle.aPosition);
325
+ gl.vertexAttribPointer(bundle.aPosition, 2, gl.FLOAT, false, 0, 0);
326
+ if (!this.texCoordBuffer) {
327
+ this.texCoordBuffer = gl.createBuffer();
328
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordBuffer);
329
+ gl.bufferData(gl.ARRAY_BUFFER, TEX_COORDS, gl.STATIC_DRAW);
330
+ }
331
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordBuffer);
332
+ gl.enableVertexAttribArray(bundle.aTexCoord);
333
+ gl.vertexAttribPointer(bundle.aTexCoord, 2, gl.FLOAT, false, 0, 0);
334
+ }
335
+ link(vertexSource, fragmentSource) {
336
+ const gl = this.gl;
337
+ const vertex = this.compile(gl.VERTEX_SHADER, vertexSource);
338
+ const fragment = this.compile(gl.FRAGMENT_SHADER, fragmentSource);
339
+ if (!vertex || !fragment)
340
+ return null;
341
+ const program = gl.createProgram();
342
+ if (!program) {
343
+ this.options.onError?.("program_allocation_failed");
344
+ return null;
345
+ }
346
+ gl.attachShader(program, vertex);
347
+ gl.attachShader(program, fragment);
348
+ gl.linkProgram(program);
349
+ // The shaders are owned by the program once linked; keeping them alive leaks on every restore.
350
+ gl.deleteShader(vertex);
351
+ gl.deleteShader(fragment);
352
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
353
+ const log = gl.getProgramInfoLog(program) ?? "";
354
+ gl.deleteProgram(program);
355
+ previewTraceWarn("webgl_program_link_failed", { log: log.slice(0, 120) });
356
+ this.options.onError?.(`program_link_failed: ${log.slice(0, 120)}`);
357
+ return null;
358
+ }
359
+ return program;
360
+ }
361
+ compile(type, source) {
362
+ const gl = this.gl;
363
+ const shader = gl.createShader(type);
364
+ if (!shader) {
365
+ this.options.onError?.("shader_allocation_failed");
366
+ return null;
367
+ }
368
+ gl.shaderSource(shader, source);
369
+ gl.compileShader(shader);
370
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
371
+ const log = gl.getShaderInfoLog(shader) ?? "";
372
+ gl.deleteShader(shader);
373
+ previewTraceWarn("webgl_shader_compile_failed", { log: log.slice(0, 120) });
374
+ this.options.onError?.(`shader_compile_failed: ${log.slice(0, 120)}`);
375
+ return null;
376
+ }
377
+ return shader;
378
+ }
379
+ }
380
+ /** Create the renderer, or null when this WebView has no WebGL1 context to give. */
381
+ export function createYuvRenderer(canvas, options = {}) {
382
+ const attributes = {
383
+ alpha: false,
384
+ antialias: false,
385
+ depth: false,
386
+ stencil: false,
387
+ preserveDrawingBuffer: false,
388
+ powerPreference: "low-power",
389
+ };
390
+ const gl = canvas.getContext("webgl", attributes) ??
391
+ canvas.getContext("experimental-webgl", attributes);
392
+ if (!gl) {
393
+ options.onError?.("webgl_unavailable");
394
+ return null;
395
+ }
396
+ return new WebGlYuvRenderer(canvas, gl, options);
397
+ }
398
+ //# sourceMappingURL=yuvRenderer.js.map