@sailfish-ai/recorder 1.12.5 → 1.12.7
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 +8 -0
- package/dist/canvas/adapters.js +382 -0
- package/dist/canvas/adapters.js.br +0 -0
- package/dist/canvas/adapters.js.gz +0 -0
- package/dist/canvas/canvasManager.js +1949 -0
- package/dist/canvas/canvasManager.js.br +0 -0
- package/dist/canvas/canvasManager.js.gz +0 -0
- package/dist/canvas/canvasPrivacy.js +233 -0
- package/dist/canvas/canvasPrivacy.js.br +0 -0
- package/dist/canvas/canvasPrivacy.js.gz +0 -0
- package/dist/canvas/codec.js +314 -0
- package/dist/canvas/codec.js.br +0 -0
- package/dist/canvas/codec.js.gz +0 -0
- package/dist/canvas/deviceClass.js +67 -0
- package/dist/canvas/deviceClass.js.br +0 -0
- package/dist/canvas/deviceClass.js.gz +0 -0
- package/dist/canvas/encodeWorker.js +284 -0
- package/dist/canvas/encodeWorker.js.br +0 -0
- package/dist/canvas/encodeWorker.js.gz +0 -0
- package/dist/canvas/governor.js +79 -0
- package/dist/canvas/governor.js.br +0 -0
- package/dist/canvas/governor.js.gz +0 -0
- package/dist/canvas/keyframes.js +183 -0
- package/dist/canvas/keyframes.js.br +0 -0
- package/dist/canvas/keyframes.js.gz +0 -0
- package/dist/canvas/mediaRecorderCapture.js +257 -0
- package/dist/canvas/mediaRecorderCapture.js.br +0 -0
- package/dist/canvas/mediaRecorderCapture.js.gz +0 -0
- package/dist/canvas/objectLog.js +150 -0
- package/dist/canvas/objectLog.js.br +0 -0
- package/dist/canvas/objectLog.js.gz +0 -0
- package/dist/canvas/patch2d.js +1302 -0
- package/dist/canvas/patch2d.js.br +0 -0
- package/dist/canvas/patch2d.js.gz +0 -0
- package/dist/canvas/patchGL.js +371 -0
- package/dist/canvas/patchGL.js.br +0 -0
- package/dist/canvas/patchGL.js.gz +0 -0
- package/dist/canvas/patchWebGPU.js +361 -0
- package/dist/canvas/patchWebGPU.js.br +0 -0
- package/dist/canvas/patchWebGPU.js.gz +0 -0
- package/dist/canvas/piiDetect.js +155 -0
- package/dist/canvas/piiDetect.js.br +0 -0
- package/dist/canvas/piiDetect.js.gz +0 -0
- package/dist/canvas/protocol.js +122 -0
- package/dist/canvas/protocol.js.br +0 -0
- package/dist/canvas/protocol.js.gz +0 -0
- package/dist/canvas/videoCapture.js +252 -0
- package/dist/canvas/videoCapture.js.br +0 -0
- package/dist/canvas/videoCapture.js.gz +0 -0
- package/dist/canvas/workerRelay.js +99 -0
- package/dist/canvas/workerRelay.js.br +0 -0
- package/dist/canvas/workerRelay.js.gz +0 -0
- package/dist/canvas/workerShim.js +227 -0
- package/dist/canvas/workerShim.js.br +0 -0
- package/dist/canvas/workerShim.js.gz +0 -0
- package/dist/canvas-worker.cjs +74 -0
- package/dist/canvas-worker.cjs.br +0 -0
- package/dist/canvas-worker.cjs.gz +0 -0
- package/dist/canvas-worker.js +78 -0
- package/dist/canvas-worker.js.br +0 -0
- package/dist/canvas-worker.js.gz +0 -0
- package/dist/canvasFrameStore.js +198 -0
- package/dist/canvasFrameStore.js.br +0 -0
- package/dist/canvasFrameStore.js.gz +0 -0
- package/dist/chunks/canvasManager-B9uQ-PFS.js +1456 -0
- package/dist/chunks/canvasManager-B9uQ-PFS.js.br +0 -0
- package/dist/chunks/canvasManager-B9uQ-PFS.js.gz +0 -0
- package/dist/chunks/canvasManager-Ce7vazTt.js +1440 -0
- package/dist/chunks/canvasManager-Ce7vazTt.js.br +0 -0
- package/dist/chunks/canvasManager-Ce7vazTt.js.gz +0 -0
- package/dist/chunks/{chunkSerializer-pkpgT-FH.js → chunkSerializer-B-z7p0r-.js} +1 -1
- package/dist/chunks/chunkSerializer-B-z7p0r-.js.br +0 -0
- package/dist/chunks/chunkSerializer-B-z7p0r-.js.gz +0 -0
- package/dist/chunks/{chunkSerializer-Cd8y9tID.js → chunkSerializer-C1knjCQ5.js} +1 -1
- package/dist/chunks/chunkSerializer-C1knjCQ5.js.br +0 -0
- package/dist/chunks/chunkSerializer-C1knjCQ5.js.gz +0 -0
- package/dist/chunks/{index-CXvHbLRT.js → index-BMHQ-0NY.js} +1115 -724
- package/dist/chunks/index-BMHQ-0NY.js.br +0 -0
- package/dist/chunks/index-BMHQ-0NY.js.gz +0 -0
- package/dist/chunks/{index-Bvd8oTpV.js → index-qgM1hFXO.js} +1025 -655
- package/dist/chunks/index-qgM1hFXO.js.br +0 -0
- package/dist/chunks/index-qgM1hFXO.js.gz +0 -0
- package/dist/chunks/patch2d-9voowhJ_.js +816 -0
- package/dist/chunks/patch2d-9voowhJ_.js.br +0 -0
- package/dist/chunks/patch2d-9voowhJ_.js.gz +0 -0
- package/dist/chunks/patch2d-PQxcaE6r.js +886 -0
- package/dist/chunks/patch2d-PQxcaE6r.js.br +0 -0
- package/dist/chunks/patch2d-PQxcaE6r.js.gz +0 -0
- package/dist/graphql.js +12 -1
- package/dist/graphql.js.br +0 -0
- package/dist/graphql.js.gz +0 -0
- package/dist/inAppReportIssueModal/index.js +10 -1
- package/dist/inAppReportIssueModal/index.js.br +0 -0
- package/dist/inAppReportIssueModal/index.js.gz +0 -0
- package/dist/index.js +144 -2
- package/dist/index.js.br +0 -0
- package/dist/index.js.gz +0 -0
- package/dist/recorder.cjs +2 -2
- package/dist/recorder.cjs.br +0 -0
- package/dist/recorder.cjs.gz +0 -0
- package/dist/recorder.js +52 -41
- package/dist/recorder.js.br +0 -0
- package/dist/recorder.js.gz +0 -0
- package/dist/recorder.umd.cjs +4842 -2215
- package/dist/recorder.umd.cjs.br +0 -0
- package/dist/recorder.umd.cjs.gz +0 -0
- package/dist/recording.js +119 -10
- package/dist/recording.js.br +0 -0
- package/dist/recording.js.gz +0 -0
- package/dist/types/canvas/adapters.d.ts +71 -0
- package/dist/types/canvas/canvasManager.d.ts +273 -0
- package/dist/types/canvas/canvasPrivacy.d.ts +184 -0
- package/dist/types/canvas/codec.d.ts +27 -0
- package/dist/types/canvas/deviceClass.d.ts +40 -0
- package/dist/types/canvas/encodeWorker.d.ts +54 -0
- package/dist/types/canvas/governor.d.ts +44 -0
- package/dist/types/canvas/keyframes.d.ts +81 -0
- package/dist/types/canvas/mediaRecorderCapture.d.ts +91 -0
- package/dist/types/canvas/objectLog.d.ts +75 -0
- package/dist/types/canvas/patch2d.d.ts +339 -0
- package/dist/types/canvas/patchGL.d.ts +74 -0
- package/dist/types/canvas/patchWebGPU.d.ts +37 -0
- package/dist/types/canvas/piiDetect.d.ts +60 -0
- package/dist/types/canvas/protocol.d.ts +176 -0
- package/dist/types/canvas/videoCapture.d.ts +67 -0
- package/dist/types/canvas/workerRelay.d.ts +29 -0
- package/dist/types/canvas/workerShim.d.ts +10 -0
- package/dist/types/canvasFrameStore.d.ts +23 -0
- package/dist/types/graphql.d.ts +1 -1
- package/dist/types/index.d.ts +99 -1
- package/dist/types/types.d.ts +40 -0
- package/dist/types/websocket.d.ts +19 -0
- package/dist/websocket.js +210 -0
- package/dist/websocket.js.br +0 -0
- package/dist/websocket.js.gz +0 -0
- package/package.json +7 -1
- package/dist/chunks/chunkSerializer-Cd8y9tID.js.br +0 -0
- package/dist/chunks/chunkSerializer-Cd8y9tID.js.gz +0 -0
- package/dist/chunks/chunkSerializer-pkpgT-FH.js.br +0 -0
- package/dist/chunks/chunkSerializer-pkpgT-FH.js.gz +0 -0
- package/dist/chunks/index-Bvd8oTpV.js.br +0 -0
- package/dist/chunks/index-Bvd8oTpV.js.gz +0 -0
- package/dist/chunks/index-CXvHbLRT.js.br +0 -0
- package/dist/chunks/index-CXvHbLRT.js.gz +0 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A41 — MediaRecorder fallback video capture (SDK half), for browsers that have
|
|
3
|
+
* NO WebCodecs (`VideoEncoder` / `VideoFrame`) but DO have `MediaRecorder` +
|
|
4
|
+
* `HTMLCanvasElement.captureStream` (notably Safari < 26). Without this, such a
|
|
5
|
+
* canvas would degrade to keyframe-only; here we capture continuous video via
|
|
6
|
+
* MediaRecorder instead.
|
|
7
|
+
*
|
|
8
|
+
* Wire contract (the player agent implements the exact mirror):
|
|
9
|
+
* - ONE `video-init` SFCV frame, header
|
|
10
|
+
* { kind:"video-init", codec:"mediarecorder", mimeType:<recorder.mimeType>, w, h }
|
|
11
|
+
* (no binary payload).
|
|
12
|
+
* - `video-chunk` SFCV frames, header { kind:"video-chunk", codec:"mediarecorder", seq }
|
|
13
|
+
* each carrying ONE `MediaRecorder.ondataavailable` Blob's bytes as the
|
|
14
|
+
* payload. The FIRST webm chunk holds the init segment, so ORDER MUST be
|
|
15
|
+
* preserved (seq is monotonic, assigned in dataavailable order).
|
|
16
|
+
*
|
|
17
|
+
* Differences from VideoCapture (WebCodecs):
|
|
18
|
+
* - No same-task readback hazard: MediaRecorder pulls frames off the
|
|
19
|
+
* captureStream track itself, so there is no afterRender pixel grab and no
|
|
20
|
+
* guardrail-#6 concern. The manager still calls afterRender for dirtiness
|
|
21
|
+
* bookkeeping, but it is a no-op here.
|
|
22
|
+
* - Chunk bytes are read from the Blob asynchronously (blob.arrayBuffer());
|
|
23
|
+
* emission is chained so seq order is preserved even if reads resolve out
|
|
24
|
+
* of order.
|
|
25
|
+
*
|
|
26
|
+
* SSR rule: nothing here touches window/document at module scope; capability
|
|
27
|
+
* detection and all DOM/Media access happen inside methods called from the
|
|
28
|
+
* manager (which itself only runs in the browser).
|
|
29
|
+
*/
|
|
30
|
+
const TIMESLICE_MS = 1_000; // ~1s cadence so chunks flow steadily
|
|
31
|
+
const CAPTURE_FPS = 30;
|
|
32
|
+
/** webm mime candidates, preferred first. Safari < 26 generally supports
|
|
33
|
+
* `video/mp4` via MediaRecorder; we still probe webm first for player parity
|
|
34
|
+
* (the player decodes whatever mimeType we report). */
|
|
35
|
+
const MIME_CANDIDATES = [
|
|
36
|
+
"video/webm;codecs=vp9",
|
|
37
|
+
"video/webm;codecs=vp8",
|
|
38
|
+
"video/webm",
|
|
39
|
+
"video/mp4",
|
|
40
|
+
];
|
|
41
|
+
/**
|
|
42
|
+
* Feature detection (contract): MediaRecorder fallback is usable ONLY when
|
|
43
|
+
* `MediaRecorder`, `HTMLCanvasElement.prototype.captureStream`, and
|
|
44
|
+
* `MediaRecorder.isTypeSupported` for at least one candidate mime are present.
|
|
45
|
+
* Callers prefer WebCodecs and reach here only when WebCodecs is ABSENT.
|
|
46
|
+
*/
|
|
47
|
+
export function mediaRecorderSupported() {
|
|
48
|
+
if (typeof MediaRecorder === "undefined")
|
|
49
|
+
return false;
|
|
50
|
+
if (typeof HTMLCanvasElement === "undefined")
|
|
51
|
+
return false;
|
|
52
|
+
if (typeof HTMLCanvasElement.prototype.captureStream !== "function")
|
|
53
|
+
return false;
|
|
54
|
+
if (typeof MediaRecorder.isTypeSupported !== "function")
|
|
55
|
+
return false;
|
|
56
|
+
return pickMime() !== null;
|
|
57
|
+
}
|
|
58
|
+
/** First candidate mime MediaRecorder claims to support, or null. */
|
|
59
|
+
export function pickMime() {
|
|
60
|
+
if (typeof MediaRecorder === "undefined" || typeof MediaRecorder.isTypeSupported !== "function") {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
for (const m of MIME_CANDIDATES) {
|
|
64
|
+
try {
|
|
65
|
+
if (MediaRecorder.isTypeSupported(m))
|
|
66
|
+
return m;
|
|
67
|
+
}
|
|
68
|
+
catch { /* probe next */ }
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
export class MediaRecorderCapture {
|
|
73
|
+
canvas;
|
|
74
|
+
out;
|
|
75
|
+
recorder = null;
|
|
76
|
+
stream = null;
|
|
77
|
+
stopped = false;
|
|
78
|
+
started = false;
|
|
79
|
+
seq = 0;
|
|
80
|
+
/** chain Blob -> bytes emission so seq order is preserved across async reads */
|
|
81
|
+
emitChain = Promise.resolve();
|
|
82
|
+
constructor(canvas, out) {
|
|
83
|
+
this.canvas = canvas;
|
|
84
|
+
this.out = out;
|
|
85
|
+
}
|
|
86
|
+
static supported() {
|
|
87
|
+
return mediaRecorderSupported();
|
|
88
|
+
}
|
|
89
|
+
/** Resolves true once the recorder started; false -> keyframes-only fallback. */
|
|
90
|
+
async start() {
|
|
91
|
+
if (this.stopped)
|
|
92
|
+
return false;
|
|
93
|
+
const w = this.canvas.width;
|
|
94
|
+
const h = this.canvas.height;
|
|
95
|
+
if (!w || !h) {
|
|
96
|
+
this.out.note("zero-size canvas — MediaRecorder capture unavailable");
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
const mime = pickMime();
|
|
100
|
+
if (!mime) {
|
|
101
|
+
this.out.note("no MediaRecorder-supported mime — keyframes only");
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
let stream;
|
|
105
|
+
try {
|
|
106
|
+
stream = this.canvas.captureStream(CAPTURE_FPS);
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
this.out.note(`captureStream failed: ${e.message}`);
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
let recorder;
|
|
113
|
+
try {
|
|
114
|
+
recorder = new MediaRecorder(stream, { mimeType: mime });
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
this.out.note(`MediaRecorder construct failed: ${e.message}`);
|
|
118
|
+
try {
|
|
119
|
+
stopStream(stream);
|
|
120
|
+
}
|
|
121
|
+
catch { /* best effort */ }
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
this.stream = stream;
|
|
125
|
+
this.recorder = recorder;
|
|
126
|
+
recorder.ondataavailable = (e) => this.onData(e.data);
|
|
127
|
+
recorder.onerror = (e) => {
|
|
128
|
+
this.out.note(`MediaRecorder error: ${String(e.error ?? e)}`);
|
|
129
|
+
};
|
|
130
|
+
try {
|
|
131
|
+
// timeslice => periodic dataavailable (~1s) so chunks stream live.
|
|
132
|
+
recorder.start(TIMESLICE_MS);
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
this.out.note(`MediaRecorder start failed: ${e.message}`);
|
|
136
|
+
this.teardownStream();
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
this.started = true;
|
|
140
|
+
// The mimeType the recorder actually negotiated may differ from the
|
|
141
|
+
// requested one (e.g. codecs appended); report the recorder's own value so
|
|
142
|
+
// the player decodes against the truth.
|
|
143
|
+
this.out.onInit(recorder.mimeType || mime, w, h);
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
/** A non-empty Blob from dataavailable. Read bytes async, emit in seq order. */
|
|
147
|
+
onData(blob) {
|
|
148
|
+
if (!blob || blob.size === 0)
|
|
149
|
+
return;
|
|
150
|
+
const seq = this.seq++;
|
|
151
|
+
this.emitChain = this.emitChain.then(async () => {
|
|
152
|
+
try {
|
|
153
|
+
const buf = await blob.arrayBuffer();
|
|
154
|
+
if (buf.byteLength > 0)
|
|
155
|
+
this.out.onChunk(seq, buf);
|
|
156
|
+
}
|
|
157
|
+
catch (e) {
|
|
158
|
+
this.out.note(`chunk read failed: ${e.message}`);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The manager's microtask pump calls this after every render for dirtiness
|
|
164
|
+
* bookkeeping. MediaRecorder pulls frames off the captureStream track on its
|
|
165
|
+
* own, so this is intentionally a no-op (kept for VideoCapture parity). The
|
|
166
|
+
* `_t` mirrors VideoCapture.afterRender's signature.
|
|
167
|
+
*/
|
|
168
|
+
afterRender(_t) { }
|
|
169
|
+
/** Interaction boost is a no-op: the captureStream fps is fixed and the
|
|
170
|
+
* recorder cannot be told to key on demand. Kept for VideoCapture parity. */
|
|
171
|
+
onInteraction(_tMs) { }
|
|
172
|
+
/**
|
|
173
|
+
* Non-terminal drain (visibilitychange:hidden): force the recorder to emit a
|
|
174
|
+
* chunk for everything buffered so far WITHOUT stopping, so capture resumes if
|
|
175
|
+
* the tab returns. Mirrors VideoCapture.flush(). No-op once stopped.
|
|
176
|
+
*/
|
|
177
|
+
async flush() {
|
|
178
|
+
if (this.stopped || !this.recorder)
|
|
179
|
+
return;
|
|
180
|
+
try {
|
|
181
|
+
if (this.recorder.state === "recording")
|
|
182
|
+
this.recorder.requestData();
|
|
183
|
+
}
|
|
184
|
+
catch { /* best effort */ }
|
|
185
|
+
// let the synchronous-ish dataavailable enqueue, then settle the chain
|
|
186
|
+
await Promise.resolve();
|
|
187
|
+
await this.emitChain.catch(() => { });
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Terminal stop: flush the final chunk (requestData then stop fire a last
|
|
191
|
+
* dataavailable), settle the emission chain so the final bytes reach onChunk,
|
|
192
|
+
* then release the recorder + stream tracks. Idempotent.
|
|
193
|
+
*/
|
|
194
|
+
async stop() {
|
|
195
|
+
if (this.stopped)
|
|
196
|
+
return;
|
|
197
|
+
this.stopped = true;
|
|
198
|
+
const recorder = this.recorder;
|
|
199
|
+
if (recorder && this.started) {
|
|
200
|
+
const lastData = new Promise((resolve) => {
|
|
201
|
+
const done = () => resolve();
|
|
202
|
+
recorder.addEventListener("stop", done, { once: true });
|
|
203
|
+
// safety: never hang the manager's stop on a wedged recorder
|
|
204
|
+
setTimeout(done, 3_000);
|
|
205
|
+
});
|
|
206
|
+
try {
|
|
207
|
+
if (recorder.state !== "inactive") {
|
|
208
|
+
recorder.requestData(); // flush the tail timeslice
|
|
209
|
+
recorder.stop(); // fires a final dataavailable then 'stop'
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch { /* already inactive */ }
|
|
213
|
+
await lastData;
|
|
214
|
+
}
|
|
215
|
+
// settle any in-flight blob reads so their chunks are emitted before teardown
|
|
216
|
+
await this.emitChain.catch(() => { });
|
|
217
|
+
this.teardownStream();
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Immediate teardown WITHOUT flushing (A22/A6/A10 parity): the canvas is gone
|
|
221
|
+
* or must not be recorded, so drop the recorder and stop the stream tracks
|
|
222
|
+
* without emitting the tail. Idempotent.
|
|
223
|
+
*/
|
|
224
|
+
abort() {
|
|
225
|
+
if (this.stopped)
|
|
226
|
+
return;
|
|
227
|
+
this.stopped = true;
|
|
228
|
+
try {
|
|
229
|
+
if (this.recorder && this.recorder.state !== "inactive")
|
|
230
|
+
this.recorder.stop();
|
|
231
|
+
}
|
|
232
|
+
catch { /* best effort */ }
|
|
233
|
+
this.teardownStream();
|
|
234
|
+
}
|
|
235
|
+
teardownStream() {
|
|
236
|
+
if (this.recorder) {
|
|
237
|
+
this.recorder.ondataavailable = null;
|
|
238
|
+
this.recorder.onerror = null;
|
|
239
|
+
this.recorder = null;
|
|
240
|
+
}
|
|
241
|
+
if (this.stream) {
|
|
242
|
+
stopStream(this.stream);
|
|
243
|
+
this.stream = null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function stopStream(stream) {
|
|
248
|
+
try {
|
|
249
|
+
for (const track of stream.getTracks()) {
|
|
250
|
+
try {
|
|
251
|
+
track.stop();
|
|
252
|
+
}
|
|
253
|
+
catch { /* best effort */ }
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch { /* getTracks unavailable */ }
|
|
257
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-context canvas object logging — the global creation log for
|
|
3
|
+
* CanvasGradient / CanvasPattern / Path2D objects.
|
|
4
|
+
*
|
|
5
|
+
* Gradients, patterns, and Path2Ds are OPAQUE: their stops/segments cannot be
|
|
6
|
+
* read back after construction, so they can only be captured by observing the
|
|
7
|
+
* factory call live. They may also be created on one context (or no context —
|
|
8
|
+
* Konva builds gradients on a hidden dummy context) and used on another, so
|
|
9
|
+
* creation is logged GLOBALLY here, and each recorder lazily emits its own
|
|
10
|
+
* copy into whichever command stream first references the object
|
|
11
|
+
* (patch2d.ts serializeFactoryObject).
|
|
12
|
+
*
|
|
13
|
+
* ROOT-CAUSE GUARDRAIL (Phase 0.5 E2E, found on /2d-objects): app scene code
|
|
14
|
+
* typically constructs these objects ONCE at startup — often before the SDK's
|
|
15
|
+
* settings fetch + session start complete and startCanvasRecording() installs
|
|
16
|
+
* the full capture hooks. Anything created in that gap is permanently
|
|
17
|
+
* unserializable ({$:"unsupported"}). This module is therefore dependency-free
|
|
18
|
+
* and cheap so index.tsx can install it SYNCHRONOUSLY at startRecording()
|
|
19
|
+
* entry (Phase 1 critical interceptors) whenever canvas recording is opted
|
|
20
|
+
* in — long before any network roundtrip.
|
|
21
|
+
*
|
|
22
|
+
* SSR rule (veritas/jsts-frontend/CLAUDE.md): no window/document/navigator or
|
|
23
|
+
* canvas globals at module scope; all installation happens inside the
|
|
24
|
+
* exported install functions, each guarded with typeof checks.
|
|
25
|
+
*/
|
|
26
|
+
/** factory-created object -> its construction + method-call history */
|
|
27
|
+
export const objectLogs = new WeakMap();
|
|
28
|
+
/**
|
|
29
|
+
* A26 — per-object recorded-calls cap. Gradients/patterns/Path2Ds are normally
|
|
30
|
+
* built with a handful of stops/segments at construction; a few apps, though,
|
|
31
|
+
* reuse one long-lived object and keep appending to it for the whole session
|
|
32
|
+
* (e.g. a Path2D that accumulates points), growing `calls[]` without bound and
|
|
33
|
+
* leaking memory. We cap the recorded history: the first MAX_OBJECT_CALLS calls
|
|
34
|
+
* are kept (covering every realistic construction), and anything beyond is
|
|
35
|
+
* dropped with a one-time marker. Replay of the common case is unaffected.
|
|
36
|
+
*/
|
|
37
|
+
export const MAX_OBJECT_CALLS = 4_096;
|
|
38
|
+
/** Append a method call to an object's log, honoring the per-object cap. */
|
|
39
|
+
export function pushObjectCall(log, method, args) {
|
|
40
|
+
if (!log)
|
|
41
|
+
return;
|
|
42
|
+
if (log.calls.length >= MAX_OBJECT_CALLS) {
|
|
43
|
+
log.callsCapped = true;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
log.calls.push({ method, args });
|
|
47
|
+
}
|
|
48
|
+
export const OBJECT_FACTORIES = [
|
|
49
|
+
"createLinearGradient",
|
|
50
|
+
"createRadialGradient",
|
|
51
|
+
"createConicGradient",
|
|
52
|
+
"createPattern",
|
|
53
|
+
];
|
|
54
|
+
export const PATH_OPS = [
|
|
55
|
+
"addPath", "arc", "arcTo", "bezierCurveTo", "closePath", "ellipse",
|
|
56
|
+
"lineTo", "moveTo", "quadraticCurveTo", "rect", "roundRect",
|
|
57
|
+
];
|
|
58
|
+
let nativePath2D = null;
|
|
59
|
+
/** The unwrapped Path2D constructor (set once Path2D logging installs). */
|
|
60
|
+
export function getNativePath2D() {
|
|
61
|
+
return nativePath2D;
|
|
62
|
+
}
|
|
63
|
+
/** prototypes whose factory methods are already wrapped */
|
|
64
|
+
const patchedFactoryProtos = new WeakSet();
|
|
65
|
+
let sharedLoggingInstalled = false;
|
|
66
|
+
/**
|
|
67
|
+
* Wrap the gradient/pattern factory methods of one 2D-context prototype
|
|
68
|
+
* (CanvasRenderingContext2D on the main thread, OffscreenCanvasRendering-
|
|
69
|
+
* Context2D inside workers) so every created object lands in `objectLogs`.
|
|
70
|
+
* Idempotent per prototype.
|
|
71
|
+
*/
|
|
72
|
+
export function installFactoryLoggingOnProto(protoObj) {
|
|
73
|
+
if (patchedFactoryProtos.has(protoObj))
|
|
74
|
+
return;
|
|
75
|
+
patchedFactoryProtos.add(protoObj);
|
|
76
|
+
const proto = protoObj;
|
|
77
|
+
for (const name of OBJECT_FACTORIES) {
|
|
78
|
+
const desc = Object.getOwnPropertyDescriptor(protoObj, name);
|
|
79
|
+
if (!desc || typeof desc.value !== "function")
|
|
80
|
+
continue;
|
|
81
|
+
const orig = desc.value;
|
|
82
|
+
proto[name] = function (...args) {
|
|
83
|
+
const result = orig.apply(this, args);
|
|
84
|
+
// log unconditionally — the object may be used on a DIFFERENT
|
|
85
|
+
// context than the one that created it (Konva's dummy context).
|
|
86
|
+
if (result && typeof result === "object") {
|
|
87
|
+
objectLogs.set(result, { factory: name, args, calls: [] });
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Wrap the realm-global object types: CanvasGradient.addColorStop appends to
|
|
95
|
+
* the creating factory's log; Path2D construction + segment ops are logged
|
|
96
|
+
* per instance (replayed via __create '__path2d' + __objcall when a path is
|
|
97
|
+
* first used on a recorded context). Idempotent.
|
|
98
|
+
*/
|
|
99
|
+
export function installSharedObjectLogging() {
|
|
100
|
+
if (sharedLoggingInstalled)
|
|
101
|
+
return;
|
|
102
|
+
sharedLoggingInstalled = true;
|
|
103
|
+
// gradient stops: append to the object's global log (cross-context safe)
|
|
104
|
+
if (typeof CanvasGradient !== "undefined") {
|
|
105
|
+
const orig = CanvasGradient.prototype.addColorStop;
|
|
106
|
+
CanvasGradient.prototype.addColorStop = function (...args) {
|
|
107
|
+
pushObjectCall(objectLogs.get(this), "addColorStop", args);
|
|
108
|
+
return orig.apply(this, args);
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (typeof Path2D !== "undefined" && !nativePath2D) {
|
|
112
|
+
nativePath2D = Path2D;
|
|
113
|
+
for (const m of PATH_OPS) {
|
|
114
|
+
const orig = Path2D.prototype[m];
|
|
115
|
+
if (typeof orig !== "function")
|
|
116
|
+
continue;
|
|
117
|
+
Path2D.prototype[m] = function (...args) {
|
|
118
|
+
pushObjectCall(objectLogs.get(this), m, args);
|
|
119
|
+
return orig.apply(this, args);
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const native = nativePath2D;
|
|
123
|
+
const Wrapped = function (arg) {
|
|
124
|
+
const instance = arg === undefined ? new native() : new native(arg);
|
|
125
|
+
objectLogs.set(instance, {
|
|
126
|
+
factory: "__path2d",
|
|
127
|
+
args: [typeof arg === "string" ? arg : null],
|
|
128
|
+
calls: arg instanceof native ? [...(objectLogs.get(arg)?.calls ?? [])] : [],
|
|
129
|
+
});
|
|
130
|
+
return instance;
|
|
131
|
+
};
|
|
132
|
+
Wrapped.prototype = native.prototype;
|
|
133
|
+
globalThis.Path2D = Wrapped;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Early hook for the SDK entry path: arm all object logging on the main
|
|
138
|
+
* thread BEFORE app scene code constructs its gradients/patterns/paths.
|
|
139
|
+
* Safe to call multiple times; no-ops outside a browser realm.
|
|
140
|
+
*/
|
|
141
|
+
export function installEarlyObjectLogging() {
|
|
142
|
+
if (typeof CanvasRenderingContext2D !== "undefined") {
|
|
143
|
+
installFactoryLoggingOnProto(CanvasRenderingContext2D.prototype);
|
|
144
|
+
}
|
|
145
|
+
if (typeof OffscreenCanvasRenderingContext2D !== "undefined") {
|
|
146
|
+
// main-thread OffscreenCanvas 2D contexts share the same object types
|
|
147
|
+
installFactoryLoggingOnProto(OffscreenCanvasRenderingContext2D.prototype);
|
|
148
|
+
}
|
|
149
|
+
installSharedObjectLogging();
|
|
150
|
+
}
|
|
Binary file
|
|
Binary file
|