@thumbmarkjs/thumbmarkjs 1.8.1 → 1.9.1
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 +9 -4
- package/dist/thumbmark.cjs.js +1 -1
- package/dist/thumbmark.cjs.js.map +1 -1
- package/dist/thumbmark.esm.js +1 -1
- package/dist/thumbmark.esm.js.map +1 -1
- package/dist/thumbmark.umd.js +1 -1
- package/dist/thumbmark.umd.js.map +1 -1
- package/dist/types/components/audio/index.d.ts +2 -0
- package/dist/types/components/canvas/index.d.ts +3 -0
- package/dist/types/components/fonts/index.d.ts +4 -0
- package/dist/types/components/hardware/index.d.ts +2 -0
- package/dist/types/components/intl/index.d.ts +2 -0
- package/dist/types/components/locales/index.d.ts +2 -0
- package/dist/types/components/math/index.d.ts +2 -0
- package/dist/types/components/mathml/index.d.ts +2 -0
- package/dist/types/components/mediaDevices/index.d.ts +2 -0
- package/dist/types/components/permissions/index.d.ts +3 -0
- package/dist/types/components/plugins/index.d.ts +2 -0
- package/dist/types/components/screen/index.d.ts +2 -0
- package/dist/types/components/speech/index.d.ts +2 -0
- package/dist/types/components/system/browser.d.ts +9 -0
- package/dist/types/components/system/index.d.ts +2 -0
- package/dist/types/components/webgl/index.d.ts +13 -0
- package/dist/types/components/webrtc/index.d.ts +3 -0
- package/dist/types/factory.d.ts +66 -0
- package/dist/types/functions/api.d.ts +73 -0
- package/dist/types/functions/filterComponents.d.ts +15 -0
- package/dist/types/functions/index.d.ts +63 -0
- package/dist/types/functions/legacy_functions.d.ts +27 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/options.d.ts +76 -0
- package/dist/types/thumbmark.d.ts +28 -0
- package/dist/types/utils/cache.d.ts +23 -0
- package/dist/types/utils/commonPixels.d.ts +1 -0
- package/dist/types/utils/ephemeralIFrame.d.ts +4 -0
- package/dist/types/utils/getMostFrequent.d.ts +5 -0
- package/dist/types/utils/hash.d.ts +5 -0
- package/dist/types/utils/imageDataToDataURL.d.ts +1 -0
- package/dist/types/utils/log.d.ts +9 -0
- package/dist/types/utils/raceAll.d.ts +10 -0
- package/dist/types/utils/sort.d.ts +8 -0
- package/dist/types/utils/stableStringify.d.ts +22 -0
- package/dist/types/utils/version.d.ts +4 -0
- package/dist/types/utils/visitorId.d.ts +14 -0
- package/package.json +3 -2
- package/src/components/audio/index.ts +0 -1
- package/src/components/system/browser.ts +24 -4
- package/src/components/webgl/index.test.ts +223 -0
- package/src/components/webgl/index.ts +188 -146
- package/src/components/webrtc/index.ts +15 -38
- package/src/functions/filterComponents.test.ts +35 -0
- package/src/functions/filterComponents.ts +2 -3
- package/src/functions/index.ts +70 -10
- package/src/utils/commonPixels.ts +25 -0
- package/src/utils/raceAll.ts +22 -12
- package/src/utils/stableStringify.ts +6 -4
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import getWebGL, { __resetWebGLCache, __getWebGLCache } from './index';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Mock ImageData — jsdom does not provide a real implementation
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
class MockImageData {
|
|
7
|
+
data: Uint8ClampedArray;
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
|
|
11
|
+
constructor(dataOrWidth: Uint8ClampedArray | number, widthOrUndefined?: number, height?: number) {
|
|
12
|
+
if (dataOrWidth instanceof Uint8ClampedArray) {
|
|
13
|
+
this.data = dataOrWidth;
|
|
14
|
+
this.width = widthOrUndefined ?? 1;
|
|
15
|
+
this.height = height ?? 1;
|
|
16
|
+
} else {
|
|
17
|
+
const w = dataOrWidth as number;
|
|
18
|
+
const h = widthOrUndefined ?? 1;
|
|
19
|
+
this.data = new Uint8ClampedArray(w * h * 4);
|
|
20
|
+
this.width = w;
|
|
21
|
+
this.height = h;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
(global as any).ImageData = MockImageData;
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// WebGL mock factory — returns an object satisfying setupWebGL() and renderImage()
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
function makeGlMock(overrides: Partial<WebGLRenderingContext> = {}): WebGLRenderingContext {
|
|
31
|
+
const gl: any = {
|
|
32
|
+
VERTEX_SHADER: 35633,
|
|
33
|
+
FRAGMENT_SHADER: 35632,
|
|
34
|
+
ARRAY_BUFFER: 34962,
|
|
35
|
+
COMPILE_STATUS: 35713,
|
|
36
|
+
LINK_STATUS: 35714,
|
|
37
|
+
RGBA: 6408,
|
|
38
|
+
UNSIGNED_BYTE: 5121,
|
|
39
|
+
COLOR_BUFFER_BIT: 16384,
|
|
40
|
+
FLOAT: 5126,
|
|
41
|
+
LINES: 1,
|
|
42
|
+
drawingBufferWidth: 200,
|
|
43
|
+
drawingBufferHeight: 100,
|
|
44
|
+
createShader: jest.fn().mockReturnValue({}),
|
|
45
|
+
shaderSource: jest.fn(),
|
|
46
|
+
compileShader: jest.fn(),
|
|
47
|
+
getShaderParameter: jest.fn().mockReturnValue(true),
|
|
48
|
+
createProgram: jest.fn().mockReturnValue({}),
|
|
49
|
+
attachShader: jest.fn(),
|
|
50
|
+
linkProgram: jest.fn(),
|
|
51
|
+
getProgramParameter: jest.fn().mockReturnValue(true),
|
|
52
|
+
createBuffer: jest.fn().mockReturnValue({}),
|
|
53
|
+
bindBuffer: jest.fn(),
|
|
54
|
+
bufferData: jest.fn(),
|
|
55
|
+
useProgram: jest.fn(),
|
|
56
|
+
getAttribLocation: jest.fn().mockReturnValue(0),
|
|
57
|
+
enableVertexAttribArray: jest.fn(),
|
|
58
|
+
vertexAttribPointer: jest.fn(),
|
|
59
|
+
viewport: jest.fn(),
|
|
60
|
+
clearColor: jest.fn(),
|
|
61
|
+
clear: jest.fn(),
|
|
62
|
+
drawArrays: jest.fn(),
|
|
63
|
+
readPixels: jest.fn(),
|
|
64
|
+
...overrides,
|
|
65
|
+
};
|
|
66
|
+
return gl as WebGLRenderingContext;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Suite setup
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
let originalGetContext: any;
|
|
73
|
+
|
|
74
|
+
beforeAll(() => {
|
|
75
|
+
originalGetContext = HTMLCanvasElement.prototype.getContext;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
afterAll(() => {
|
|
79
|
+
HTMLCanvasElement.prototype.getContext = originalGetContext;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
beforeEach(() => {
|
|
83
|
+
__resetWebGLCache();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Tests
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
describe('webgl component — context-loss recovery', () => {
|
|
90
|
+
|
|
91
|
+
// Happy path: cache is populated after first successful call
|
|
92
|
+
test('first getWebGL() call populates the cache', async () => {
|
|
93
|
+
HTMLCanvasElement.prototype.getContext = jest.fn().mockReturnValue(makeGlMock()) as any;
|
|
94
|
+
|
|
95
|
+
await getWebGL();
|
|
96
|
+
|
|
97
|
+
expect(__getWebGLCache()).not.toBeNull();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Cache reuse: two calls share the same canvas object
|
|
101
|
+
test('two consecutive getWebGL() calls reuse the same cached canvas', async () => {
|
|
102
|
+
HTMLCanvasElement.prototype.getContext = jest.fn().mockReturnValue(makeGlMock()) as any;
|
|
103
|
+
|
|
104
|
+
await getWebGL();
|
|
105
|
+
const cacheAfterFirst = __getWebGLCache();
|
|
106
|
+
expect(cacheAfterFirst).not.toBeNull();
|
|
107
|
+
|
|
108
|
+
await getWebGL();
|
|
109
|
+
const cacheAfterSecond = __getWebGLCache();
|
|
110
|
+
|
|
111
|
+
// Same object reference — no new canvas was created
|
|
112
|
+
expect(cacheAfterSecond).toBe(cacheAfterFirst);
|
|
113
|
+
// getContext called exactly once (canvas created once)
|
|
114
|
+
expect(HTMLCanvasElement.prototype.getContext).toHaveBeenCalledTimes(1);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Context-loss listener: dispatching webglcontextlost clears the cache
|
|
118
|
+
test('webglcontextlost event clears the cache', async () => {
|
|
119
|
+
HTMLCanvasElement.prototype.getContext = jest.fn().mockReturnValue(makeGlMock()) as any;
|
|
120
|
+
|
|
121
|
+
await getWebGL();
|
|
122
|
+
const cacheBeforeLoss = __getWebGLCache();
|
|
123
|
+
expect(cacheBeforeLoss).not.toBeNull();
|
|
124
|
+
|
|
125
|
+
// Fire the context-lost event on the cached canvas
|
|
126
|
+
cacheBeforeLoss!.canvas.dispatchEvent(new Event('webglcontextlost', { cancelable: true }));
|
|
127
|
+
|
|
128
|
+
expect(__getWebGLCache()).toBeNull();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Context-loss listener: event.preventDefault() is called
|
|
132
|
+
test('webglcontextlost listener calls event.preventDefault()', async () => {
|
|
133
|
+
HTMLCanvasElement.prototype.getContext = jest.fn().mockReturnValue(makeGlMock()) as any;
|
|
134
|
+
|
|
135
|
+
await getWebGL();
|
|
136
|
+
const cache = __getWebGLCache();
|
|
137
|
+
expect(cache).not.toBeNull();
|
|
138
|
+
|
|
139
|
+
const lostEvent = new Event('webglcontextlost', { cancelable: true });
|
|
140
|
+
const preventDefaultSpy = jest.spyOn(lostEvent, 'preventDefault');
|
|
141
|
+
|
|
142
|
+
cache!.canvas.dispatchEvent(lostEvent);
|
|
143
|
+
|
|
144
|
+
expect(preventDefaultSpy).toHaveBeenCalledTimes(1);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// After context loss, next getWebGL() call creates a NEW canvas
|
|
148
|
+
test('getWebGL() after context loss rebuilds the cache via a new canvas', async () => {
|
|
149
|
+
const gl = makeGlMock();
|
|
150
|
+
HTMLCanvasElement.prototype.getContext = jest.fn().mockReturnValue(gl) as any;
|
|
151
|
+
|
|
152
|
+
await getWebGL();
|
|
153
|
+
const firstCache = __getWebGLCache();
|
|
154
|
+
expect(firstCache).not.toBeNull();
|
|
155
|
+
|
|
156
|
+
// Simulate context loss
|
|
157
|
+
firstCache!.canvas.dispatchEvent(new Event('webglcontextlost', { cancelable: true }));
|
|
158
|
+
expect(__getWebGLCache()).toBeNull();
|
|
159
|
+
|
|
160
|
+
// Reset mock call count, re-install for rebuild
|
|
161
|
+
HTMLCanvasElement.prototype.getContext = jest.fn().mockReturnValue(gl) as any;
|
|
162
|
+
|
|
163
|
+
await getWebGL();
|
|
164
|
+
const secondCache = __getWebGLCache();
|
|
165
|
+
expect(secondCache).not.toBeNull();
|
|
166
|
+
// A fresh cache object is created — different reference from the lost one
|
|
167
|
+
expect(secondCache).not.toBe(firstCache);
|
|
168
|
+
// getContext called once for the rebuild (new canvas)
|
|
169
|
+
expect(HTMLCanvasElement.prototype.getContext).toHaveBeenCalledTimes(1);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// once: true — the listener fires at most once per canvas
|
|
173
|
+
test('webglcontextlost listener fires only once (once:true)', async () => {
|
|
174
|
+
HTMLCanvasElement.prototype.getContext = jest.fn().mockReturnValue(makeGlMock()) as any;
|
|
175
|
+
|
|
176
|
+
await getWebGL();
|
|
177
|
+
const firstCache = __getWebGLCache();
|
|
178
|
+
const canvas = firstCache!.canvas;
|
|
179
|
+
|
|
180
|
+
// First dispatch: clears cache
|
|
181
|
+
canvas.dispatchEvent(new Event('webglcontextlost', { cancelable: true }));
|
|
182
|
+
expect(__getWebGLCache()).toBeNull();
|
|
183
|
+
|
|
184
|
+
// Second dispatch on the same canvas — listener already removed (once:true),
|
|
185
|
+
// so _cache remains null (unchanged) and no throw occurs
|
|
186
|
+
expect(() => {
|
|
187
|
+
canvas.dispatchEvent(new Event('webglcontextlost', { cancelable: true }));
|
|
188
|
+
}).not.toThrow();
|
|
189
|
+
expect(__getWebGLCache()).toBeNull();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// renderImage catch block: render failure clears the cache
|
|
193
|
+
test('render failure (useProgram throws) clears the cache', async () => {
|
|
194
|
+
HTMLCanvasElement.prototype.getContext = jest.fn().mockReturnValue(makeGlMock()) as any;
|
|
195
|
+
|
|
196
|
+
// First call populates cache
|
|
197
|
+
await getWebGL();
|
|
198
|
+
expect(__getWebGLCache()).not.toBeNull();
|
|
199
|
+
|
|
200
|
+
// Patch the cached gl to throw on the next render
|
|
201
|
+
const cache = __getWebGLCache()!;
|
|
202
|
+
(cache as any).gl.useProgram = jest.fn().mockImplementation(() => {
|
|
203
|
+
throw new Error('WebGL context lost');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// The call should not throw; it should return the 'unsupported' fallback
|
|
207
|
+
const result = await getWebGL();
|
|
208
|
+
expect(result).toEqual({ webgl: 'unsupported' });
|
|
209
|
+
|
|
210
|
+
// Cache must have been cleared by the catch block
|
|
211
|
+
expect(__getWebGLCache()).toBeNull();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Graceful degradation: getContext returns null → returns 'unsupported'
|
|
215
|
+
test('getWebGL() returns unsupported when WebGL context is unavailable', async () => {
|
|
216
|
+
HTMLCanvasElement.prototype.getContext = jest.fn().mockReturnValue(null) as any;
|
|
217
|
+
|
|
218
|
+
const result = await getWebGL();
|
|
219
|
+
|
|
220
|
+
expect(result).toEqual({ webgl: 'unsupported' });
|
|
221
|
+
expect(__getWebGLCache()).toBeNull();
|
|
222
|
+
});
|
|
223
|
+
});
|
|
@@ -1,146 +1,188 @@
|
|
|
1
|
-
import { componentInterface
|
|
2
|
-
import { hash } from '../../utils/hash'
|
|
3
|
-
import { getCommonPixels } from '../../utils/commonPixels';
|
|
4
|
-
import { getBrowser } from '../system/browser';
|
|
5
|
-
|
|
6
|
-
const _RUNS = (getBrowser().name !== 'SamsungBrowser') ? 1 : 3;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
1
|
+
import { componentInterface } from '../../factory'
|
|
2
|
+
import { hash } from '../../utils/hash'
|
|
3
|
+
import { getCommonPixels } from '../../utils/commonPixels';
|
|
4
|
+
import { getBrowser } from '../system/browser';
|
|
5
|
+
|
|
6
|
+
const _RUNS = (getBrowser().name !== 'SamsungBrowser') ? 1 : 3;
|
|
7
|
+
const _USE_CACHE = getBrowser().name !== 'Brave';
|
|
8
|
+
|
|
9
|
+
// Canvas and viewport dimensions — part of the fingerprint signal, do not change.
|
|
10
|
+
const _CANVAS_W = 200;
|
|
11
|
+
const _CANVAS_H = 100;
|
|
12
|
+
const _NUM_SPOKES = 137;
|
|
13
|
+
|
|
14
|
+
// Shader sources are constant — hoist to module scope to avoid per-call string allocation.
|
|
15
|
+
const _VERTEX_SHADER_SRC = `
|
|
16
|
+
attribute vec2 position;
|
|
17
|
+
void main() {
|
|
18
|
+
gl_Position = vec4(position, 0.0, 1.0);
|
|
19
|
+
}
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
const _FRAGMENT_SHADER_SRC = `
|
|
23
|
+
precision mediump float;
|
|
24
|
+
void main() {
|
|
25
|
+
gl_FragColor = vec4(0.812, 0.195, 0.553, 0.921); // Set line color
|
|
26
|
+
}
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
// Precompute the spoke vertices once at module load. The values are determined
|
|
30
|
+
// solely by _NUM_SPOKES, _CANVAS_W, and _CANVAS_H — all module constants —
|
|
31
|
+
// so they are identical to what the old per-call computation produced.
|
|
32
|
+
const _VERTICES: Float32Array = (() => {
|
|
33
|
+
const v = new Float32Array(_NUM_SPOKES * 4);
|
|
34
|
+
const angleIncrement = (2 * Math.PI) / _NUM_SPOKES;
|
|
35
|
+
for (let i = 0; i < _NUM_SPOKES; i++) {
|
|
36
|
+
const angle = i * angleIncrement;
|
|
37
|
+
v[i * 4] = 0; // Center X
|
|
38
|
+
v[i * 4 + 1] = 0; // Center Y
|
|
39
|
+
v[i * 4 + 2] = Math.cos(angle) * (_CANVAS_W / 2); // Endpoint X
|
|
40
|
+
v[i * 4 + 3] = Math.sin(angle) * (_CANVAS_H / 2); // Endpoint Y
|
|
41
|
+
}
|
|
42
|
+
return v;
|
|
43
|
+
})();
|
|
44
|
+
|
|
45
|
+
interface WebGLCache {
|
|
46
|
+
canvas: HTMLCanvasElement;
|
|
47
|
+
gl: WebGLRenderingContext;
|
|
48
|
+
program: WebGLProgram;
|
|
49
|
+
buffer: WebGLBuffer;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Module-scope cache — only populated when _USE_CACHE is true (non-Brave).
|
|
53
|
+
let _cache: WebGLCache | null = null;
|
|
54
|
+
|
|
55
|
+
/** Test-only: reset the module-scope cache between test runs. */
|
|
56
|
+
export function __resetWebGLCache(): void {
|
|
57
|
+
_cache = null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Test-only: read the current cache reference without mutating it. */
|
|
61
|
+
export function __getWebGLCache(): WebGLCache | null {
|
|
62
|
+
return _cache;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Create a canvas, compile shaders, link program, and upload vertex data.
|
|
67
|
+
* Returns null if any step fails so callers can degrade gracefully.
|
|
68
|
+
*/
|
|
69
|
+
function setupWebGL(): WebGLCache | null {
|
|
70
|
+
try {
|
|
71
|
+
if (typeof document === 'undefined') return null;
|
|
72
|
+
|
|
73
|
+
const canvas = document.createElement('canvas');
|
|
74
|
+
canvas.width = _CANVAS_W;
|
|
75
|
+
canvas.height = _CANVAS_H;
|
|
76
|
+
|
|
77
|
+
const gl = canvas.getContext('webgl');
|
|
78
|
+
if (!gl) return null;
|
|
79
|
+
|
|
80
|
+
// When the browser invalidates GPU resources it fires webglcontextlost.
|
|
81
|
+
// Null the cache so the next getOrInitCache() rebuilds via a fresh setupWebGL().
|
|
82
|
+
// { once: true } ensures the listener self-removes after firing so the old
|
|
83
|
+
// canvas does not keep the WebGLCache object alive after recovery.
|
|
84
|
+
canvas.addEventListener('webglcontextlost', (event) => {
|
|
85
|
+
event.preventDefault();
|
|
86
|
+
_cache = null;
|
|
87
|
+
}, { once: true });
|
|
88
|
+
|
|
89
|
+
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
|
|
90
|
+
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
|
|
91
|
+
if (!vertexShader || !fragmentShader) return null;
|
|
92
|
+
|
|
93
|
+
gl.shaderSource(vertexShader, _VERTEX_SHADER_SRC);
|
|
94
|
+
gl.compileShader(vertexShader);
|
|
95
|
+
if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) return null;
|
|
96
|
+
|
|
97
|
+
gl.shaderSource(fragmentShader, _FRAGMENT_SHADER_SRC);
|
|
98
|
+
gl.compileShader(fragmentShader);
|
|
99
|
+
if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) return null;
|
|
100
|
+
|
|
101
|
+
const program = gl.createProgram();
|
|
102
|
+
if (!program) return null;
|
|
103
|
+
|
|
104
|
+
gl.attachShader(program, vertexShader);
|
|
105
|
+
gl.attachShader(program, fragmentShader);
|
|
106
|
+
gl.linkProgram(program);
|
|
107
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) return null;
|
|
108
|
+
|
|
109
|
+
const buffer = gl.createBuffer();
|
|
110
|
+
if (!buffer) return null;
|
|
111
|
+
|
|
112
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
113
|
+
gl.bufferData(gl.ARRAY_BUFFER, _VERTICES, gl.STATIC_DRAW);
|
|
114
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, null);
|
|
115
|
+
|
|
116
|
+
return { canvas, gl, program, buffer };
|
|
117
|
+
} catch (_) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* For non-Brave browsers: lazily initialise once and reuse.
|
|
124
|
+
* For Brave: always create fresh (Brave farbles WebGL per-context, preserving
|
|
125
|
+
* the noise signal that today's per-call setup drives).
|
|
126
|
+
*/
|
|
127
|
+
function getOrInitCache(): WebGLCache | null {
|
|
128
|
+
if (_USE_CACHE) {
|
|
129
|
+
if (!_cache) _cache = setupWebGL();
|
|
130
|
+
return _cache;
|
|
131
|
+
}
|
|
132
|
+
// Brave path: fresh context every call, byte-identical behaviour to pre-cache code.
|
|
133
|
+
return setupWebGL();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Execute one render pass on the shared (or fresh) WebGL context and return
|
|
138
|
+
* the raw pixel data as an ImageData. Returns a 1×1 blank ImageData on error.
|
|
139
|
+
*/
|
|
140
|
+
function renderImage(cache: WebGLCache): ImageData {
|
|
141
|
+
const { canvas, gl, program, buffer } = cache;
|
|
142
|
+
try {
|
|
143
|
+
gl.useProgram(program);
|
|
144
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
145
|
+
|
|
146
|
+
const positionAttribute = gl.getAttribLocation(program, 'position');
|
|
147
|
+
gl.enableVertexAttribArray(positionAttribute);
|
|
148
|
+
gl.vertexAttribPointer(positionAttribute, 2, gl.FLOAT, false, 0, 0);
|
|
149
|
+
|
|
150
|
+
gl.viewport(0, 0, canvas.width, canvas.height);
|
|
151
|
+
gl.clearColor(0.0, 0.0, 0.0, 1.0);
|
|
152
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
153
|
+
gl.drawArrays(gl.LINES, 0, _NUM_SPOKES * 2);
|
|
154
|
+
|
|
155
|
+
const pixelData = new Uint8ClampedArray(canvas.width * canvas.height * 4);
|
|
156
|
+
gl.readPixels(0, 0, canvas.width, canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, pixelData);
|
|
157
|
+
return new ImageData(pixelData, canvas.width, canvas.height);
|
|
158
|
+
} catch (_) {
|
|
159
|
+
// Belt-and-suspenders: any render failure (context loss, GPU driver glitch, etc.)
|
|
160
|
+
// invalidates the cache so the next call rebuilds rather than retrying with a stale context.
|
|
161
|
+
_cache = null;
|
|
162
|
+
return new ImageData(1, 1);
|
|
163
|
+
} finally {
|
|
164
|
+
// Reset WebGL state to match pre-cache behaviour.
|
|
165
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, null);
|
|
166
|
+
gl.useProgram(null);
|
|
167
|
+
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
|
|
168
|
+
gl.clearColor(0.0, 0.0, 0.0, 0.0);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export default async function getWebGL(): Promise<componentInterface> {
|
|
173
|
+
const cache = getOrInitCache();
|
|
174
|
+
|
|
175
|
+
if (!cache) {
|
|
176
|
+
return { 'webgl': 'unsupported' };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const imageDatas: ImageData[] = Array.from({ length: _RUNS }, () => renderImage(cache));
|
|
181
|
+
const commonImageData = getCommonPixels(imageDatas, cache.canvas.width, cache.canvas.height);
|
|
182
|
+
return {
|
|
183
|
+
'commonPixelsHash': hash(commonImageData.data.toString()).toString(),
|
|
184
|
+
};
|
|
185
|
+
} catch (_) {
|
|
186
|
+
return { 'webgl': 'unsupported' };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -5,6 +5,7 @@ import { stableStringify } from '../../utils/stableStringify';
|
|
|
5
5
|
|
|
6
6
|
export default async function getWebRTC(options?: optionsInterface): Promise<componentInterface | null> {
|
|
7
7
|
return new Promise((resolve) => {
|
|
8
|
+
let connection: RTCPeerConnection | undefined;
|
|
8
9
|
try {
|
|
9
10
|
// Check if WebRTC is supported
|
|
10
11
|
const RTCPeerConnection = (window as any).RTCPeerConnection || (window as any).webkitRTCPeerConnection || (window as any).mozRTCPeerConnection;
|
|
@@ -21,14 +22,17 @@ export default async function getWebRTC(options?: optionsInterface): Promise<com
|
|
|
21
22
|
iceServers: []
|
|
22
23
|
};
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
connection
|
|
25
|
+
connection = new RTCPeerConnection(config);
|
|
26
|
+
// Non-null assertion: connection was just assigned above; if the constructor
|
|
27
|
+
// had thrown we would not reach this line.
|
|
28
|
+
const conn: RTCPeerConnection = connection!;
|
|
29
|
+
conn.createDataChannel(''); // trigger ICE gathering
|
|
26
30
|
|
|
27
31
|
const processOffer = async () => {
|
|
28
32
|
try {
|
|
29
33
|
const offerOptions = { offerToReceiveAudio: true, offerToReceiveVideo: true };
|
|
30
|
-
const offer = await
|
|
31
|
-
await
|
|
34
|
+
const offer = await conn.createOffer(offerOptions);
|
|
35
|
+
await conn.setLocalDescription(offer);
|
|
32
36
|
|
|
33
37
|
const sdp = offer.sdp || '';
|
|
34
38
|
|
|
@@ -81,39 +85,11 @@ export default async function getWebRTC(options?: optionsInterface): Promise<com
|
|
|
81
85
|
extensionsHash: hash(stableStringify(extensions))
|
|
82
86
|
};
|
|
83
87
|
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const result = await new Promise<componentInterface>((resolveResult) => {
|
|
90
|
-
const timeout = setTimeout(() => {
|
|
91
|
-
connection.removeEventListener('icecandidate', onIceCandidate);
|
|
92
|
-
connection.close();
|
|
93
|
-
resolveResult({
|
|
94
|
-
supported: true,
|
|
95
|
-
...compressedData,
|
|
96
|
-
timeout: true
|
|
97
|
-
});
|
|
98
|
-
}, iceTimeout);
|
|
99
|
-
|
|
100
|
-
const onIceCandidate = (event: RTCPeerConnectionIceEvent) => {
|
|
101
|
-
const candidateObj = event.candidate;
|
|
102
|
-
if (!candidateObj || !candidateObj.candidate) return;
|
|
103
|
-
|
|
104
|
-
clearTimeout(timeout);
|
|
105
|
-
connection.removeEventListener('icecandidate', onIceCandidate);
|
|
106
|
-
connection.close();
|
|
107
|
-
|
|
108
|
-
resolveResult({
|
|
109
|
-
supported: true,
|
|
110
|
-
...compressedData,
|
|
111
|
-
candidateType: candidateObj.type || ''
|
|
112
|
-
});
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
connection.addEventListener('icecandidate', onIceCandidate);
|
|
116
|
-
});
|
|
88
|
+
// With iceServers:[] only "host" candidates are generated, so waiting for
|
|
89
|
+
// the icecandidate event adds latency without any entropy gain. Close
|
|
90
|
+
// immediately and hard-code the known value to keep the hash stable.
|
|
91
|
+
conn.close();
|
|
92
|
+
const result = { supported: true, ...compressedData, candidateType: 'host' };
|
|
117
93
|
|
|
118
94
|
resolve({
|
|
119
95
|
details: result,
|
|
@@ -121,7 +97,7 @@ export default async function getWebRTC(options?: optionsInterface): Promise<com
|
|
|
121
97
|
});
|
|
122
98
|
|
|
123
99
|
} catch (error) {
|
|
124
|
-
|
|
100
|
+
conn.close();
|
|
125
101
|
resolve({
|
|
126
102
|
supported: true,
|
|
127
103
|
error: `WebRTC offer failed: ${(error as Error).message}`
|
|
@@ -132,6 +108,7 @@ export default async function getWebRTC(options?: optionsInterface): Promise<com
|
|
|
132
108
|
processOffer();
|
|
133
109
|
|
|
134
110
|
} catch (error) {
|
|
111
|
+
connection?.close();
|
|
135
112
|
resolve({
|
|
136
113
|
supported: false,
|
|
137
114
|
error: `WebRTC error: ${(error as Error).message}`
|