@sailfish-ai/recorder 1.12.4 → 1.12.6
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-BYn3_oyo.js +1440 -0
- package/dist/chunks/canvasManager-BYn3_oyo.js.br +0 -0
- package/dist/chunks/canvasManager-BYn3_oyo.js.gz +0 -0
- package/dist/chunks/canvasManager-CoX5072G.js +1456 -0
- package/dist/chunks/canvasManager-CoX5072G.js.br +0 -0
- package/dist/chunks/canvasManager-CoX5072G.js.gz +0 -0
- package/dist/chunks/{chunkSerializer-FQtY90Av.js → chunkSerializer-C_K0Wyk0.js} +1 -1
- package/dist/chunks/chunkSerializer-C_K0Wyk0.js.br +0 -0
- package/dist/chunks/chunkSerializer-C_K0Wyk0.js.gz +0 -0
- package/dist/chunks/{chunkSerializer-DDukZpgl.js → chunkSerializer-CghPitF4.js} +1 -1
- package/dist/chunks/chunkSerializer-CghPitF4.js.br +0 -0
- package/dist/chunks/chunkSerializer-CghPitF4.js.gz +0 -0
- package/dist/chunks/{index-C-qbsfKe.js → index-BIj4l3CV.js} +1028 -650
- package/dist/chunks/index-BIj4l3CV.js.br +0 -0
- package/dist/chunks/index-BIj4l3CV.js.gz +0 -0
- package/dist/chunks/{index-D6axlCRu.js → index-DIYomvdD.js} +1119 -720
- package/dist/chunks/index-DIYomvdD.js.br +0 -0
- package/dist/chunks/index-DIYomvdD.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 +48 -0
- 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 +4847 -2212
- 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-DDukZpgl.js.br +0 -0
- package/dist/chunks/chunkSerializer-DDukZpgl.js.gz +0 -0
- package/dist/chunks/chunkSerializer-FQtY90Av.js.br +0 -0
- package/dist/chunks/chunkSerializer-FQtY90Av.js.gz +0 -0
- package/dist/chunks/index-C-qbsfKe.js.br +0 -0
- package/dist/chunks/index-C-qbsfKe.js.gz +0 -0
- package/dist/chunks/index-D6axlCRu.js.br +0 -0
- package/dist/chunks/index-D6axlCRu.js.gz +0 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { type SensitiveContentFlags } from "./piiDetect";
|
|
2
|
+
export type CanvasPrivacyMode = "suppress" | "redact" | "normal";
|
|
3
|
+
/** Why a canvas was suppressed — rides the op:"masked" index event so the
|
|
4
|
+
* player / debugging can distinguish a block selector from the legacy
|
|
5
|
+
* mask-canvas class. */
|
|
6
|
+
export type SuppressReason = "block" | "mask";
|
|
7
|
+
/**
|
|
8
|
+
* The privacy-relevant subset of CanvasRecorderSettings. Kept as its own
|
|
9
|
+
* interface so the classifier is unit-testable without constructing a full
|
|
10
|
+
* recorder settings object.
|
|
11
|
+
*/
|
|
12
|
+
export interface CanvasPrivacySettings {
|
|
13
|
+
/** legacy mask-canvas class (default "sailfishSanitize") — suppress */
|
|
14
|
+
maskCanvasClass?: string;
|
|
15
|
+
/** rrweb maskTextClass — redact drawn text */
|
|
16
|
+
maskTextClass?: string;
|
|
17
|
+
/** rrweb maskTextSelector — redact drawn text */
|
|
18
|
+
maskTextSelector?: string;
|
|
19
|
+
/** rrweb blockSelector — suppress */
|
|
20
|
+
blockSelector?: string;
|
|
21
|
+
/** rrweb unmaskSelector — override: force normal (wins over block/mask) */
|
|
22
|
+
unmaskSelector?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Global canvas-text masking switch (the `maskCanvasText` init option).
|
|
25
|
+
* true — every recorded canvas redacts drawn text (independent of any
|
|
26
|
+
* selector), regardless of DOM masking config.
|
|
27
|
+
* false — opt OUT of the mirror-DOM default (DOM text masking does NOT
|
|
28
|
+
* propagate to canvas text). Explicit per-canvas maskText* selector
|
|
29
|
+
* matches still redact (matching a selector is explicit intent).
|
|
30
|
+
* undefined — mirror-DOM default: if DOM text masking is configured
|
|
31
|
+
* (`maskTextSelector` or `maskTextClass` set), canvas text is masked
|
|
32
|
+
* too; otherwise normal.
|
|
33
|
+
*/
|
|
34
|
+
maskCanvasText?: boolean;
|
|
35
|
+
recordSsn?: boolean;
|
|
36
|
+
recordCreditCardInfo?: boolean;
|
|
37
|
+
recordDob?: boolean;
|
|
38
|
+
}
|
|
39
|
+
/** The legacy mask-canvas selector: `.${maskCanvasClass}, [data-sf-mask]`. */
|
|
40
|
+
export declare function maskCanvasSelector(settings: CanvasPrivacySettings): string;
|
|
41
|
+
/**
|
|
42
|
+
* Classify a canvas's privacy mode. PRECEDENCE (highest first):
|
|
43
|
+
*
|
|
44
|
+
* 1. unmaskSelector ancestor/self match ⇒ "normal" (override)
|
|
45
|
+
* 2. blockSelector OR mask-canvas selector match ⇒ "suppress"
|
|
46
|
+
* 3. maskTextClass (.class) OR maskTextSelector ⇒ "redact"
|
|
47
|
+
* 3b. global maskCanvasText === true ⇒ "redact"
|
|
48
|
+
* 3c. mirror-DOM (DOM text masking configured AND
|
|
49
|
+
* maskCanvasText !== false) ⇒ "redact"
|
|
50
|
+
* 4. otherwise ⇒ "normal"
|
|
51
|
+
*
|
|
52
|
+
* NOTE on HIPAA: a HIPAA-enabled account does NOT suppress canvas recording.
|
|
53
|
+
* Canvas still records; PHI separation happens downstream (the SDK's index
|
|
54
|
+
* events ride the rrweb event pipeline, which routes to the `phi.*` sinks via
|
|
55
|
+
* the backend `is_phi` decision, and binary frames store in the company bucket
|
|
56
|
+
* exactly like raw events). Developers prevent specific sensitive canvases from
|
|
57
|
+
* being captured with the mask/block selectors below — HIPAA is a routing flag,
|
|
58
|
+
* not a capture switch.
|
|
59
|
+
*
|
|
60
|
+
* Uses ancestor-aware `closestSafe` for class/block/unmask selectors (a privacy
|
|
61
|
+
* container masks every canvas inside it) and self-or-ancestor for maskText*.
|
|
62
|
+
* Every selector call is wrapped so a malformed admin selector fails SAFE: it
|
|
63
|
+
* simply does not match (a throw would crash the recorder; the explicit
|
|
64
|
+
* mask-canvas / block backstops remain effective).
|
|
65
|
+
*/
|
|
66
|
+
export declare function classifyCanvasPrivacy(canvas: HTMLCanvasElement, settings: CanvasPrivacySettings): CanvasPrivacyMode;
|
|
67
|
+
/**
|
|
68
|
+
* Determine WHY a canvas was suppressed (for the op:"masked" reason field).
|
|
69
|
+
* Only meaningful when `classifyCanvasPrivacy` returned "suppress"; precedence
|
|
70
|
+
* matches the classifier (unmask override, then block, then mask-canvas).
|
|
71
|
+
* Returns undefined for non-suppress canvases.
|
|
72
|
+
*/
|
|
73
|
+
export declare function suppressReason(canvas: HTMLCanvasElement, settings: CanvasPrivacySettings): SuppressReason | undefined;
|
|
74
|
+
/**
|
|
75
|
+
* Full length-preserving mask: every non-whitespace char → "*", whitespace
|
|
76
|
+
* kept. Mirrors rrweb's maskTextFn (privacyMask.makeMaskTextFn) so a redacted
|
|
77
|
+
* canvas string looks identical to a redacted DOM text node. Surrogate pairs
|
|
78
|
+
* (emoji) and combining marks are each non-whitespace code units, so they
|
|
79
|
+
* become "*" per UTF-16 unit — never throws, never leaks the original glyph.
|
|
80
|
+
*/
|
|
81
|
+
export declare function maskAllText(text: string): string;
|
|
82
|
+
/**
|
|
83
|
+
* Redact a single drawn-text argument according to the canvas's privacy mode:
|
|
84
|
+
* - "redact": full mask (maskAllText) — drawn text is never legible.
|
|
85
|
+
* - "normal": content-detection redaction (redactSensitiveContent) gated on
|
|
86
|
+
* the record* flags — defense-in-depth so CC/SSN/DOB that an app
|
|
87
|
+
* paints into a non-masked canvas is still caught.
|
|
88
|
+
* - "suppress": never reaches here (a suppressed canvas records no ops); the
|
|
89
|
+
* input is returned unchanged as a defensive no-op.
|
|
90
|
+
*
|
|
91
|
+
* Non-string input (numbers, x/y coords passed as the 2nd/3rd fillText args)
|
|
92
|
+
* passes through untouched — only the FIRST string arg of fillText/strokeText
|
|
93
|
+
* is the rendered text, and the caller restricts redaction to that arg.
|
|
94
|
+
*/
|
|
95
|
+
export declare function redactCanvasText(text: unknown, mode: CanvasPrivacyMode, flags: SensitiveContentFlags): unknown;
|
|
96
|
+
/** How masked drawn text is represented. `asterisks` length-preserves (every
|
|
97
|
+
* non-space char → "*"); `bar` drops the glyphs and paints a solid redaction bar
|
|
98
|
+
* over the text's bounding box. */
|
|
99
|
+
export type TextRedactStyle = "asterisks" | "bar";
|
|
100
|
+
/** How a masked shape (rect / path fill-stroke / image) is represented.
|
|
101
|
+
* `color` — keep the SAME shape, but force its fill/stroke to REDACTION_COLOR
|
|
102
|
+
* (preserves layout; hides content).
|
|
103
|
+
* `block` — drop the shape's geometry; paint one solid REDACTION_COLOR block
|
|
104
|
+
* over its bounding box.
|
|
105
|
+
* `remove` — drop the shape entirely (for when the geometry itself is
|
|
106
|
+
* sensitive). The keyframe region is still blanked so the original
|
|
107
|
+
* pixels never ship. */
|
|
108
|
+
export type ShapeRedactStyle = "color" | "block" | "remove";
|
|
109
|
+
export interface RedactStyle {
|
|
110
|
+
text: TextRedactStyle;
|
|
111
|
+
shape: ShapeRedactStyle;
|
|
112
|
+
}
|
|
113
|
+
/** Mirror-DOM defaults: text → length-preserving asterisks (looks like a masked
|
|
114
|
+
* DOM text node), shapes → same shape recolored (layout preserved). */
|
|
115
|
+
export declare const DEFAULT_REDACT_STYLE: RedactStyle;
|
|
116
|
+
/** Dark neutral used everywhere a redaction is painted: recolored shapes + bars
|
|
117
|
+
* in the command stream AND the region-blanked keyframe pixels. A single source
|
|
118
|
+
* of truth keeps command replay and keyframe consistent. */
|
|
119
|
+
export declare const REDACTION_COLOR = "#1f2433";
|
|
120
|
+
/** Merge a per-call style override onto a base style (per-call wins; omitted
|
|
121
|
+
* fields inherit). `base` defaults to DEFAULT_REDACT_STYLE. Never mutates. */
|
|
122
|
+
export declare function resolveRedactStyle(base: RedactStyle | undefined, override?: Partial<RedactStyle> | null): RedactStyle;
|
|
123
|
+
/** An axis-aligned rectangle in device (canvas-pixel) space — the unit of
|
|
124
|
+
* keyframe region-blanking. */
|
|
125
|
+
export interface MaskRegion {
|
|
126
|
+
x: number;
|
|
127
|
+
y: number;
|
|
128
|
+
w: number;
|
|
129
|
+
h: number;
|
|
130
|
+
}
|
|
131
|
+
/** The 6-tuple of a 2D affine transform (a DOMMatrix subset). */
|
|
132
|
+
export interface Mat2D {
|
|
133
|
+
a: number;
|
|
134
|
+
b: number;
|
|
135
|
+
c: number;
|
|
136
|
+
d: number;
|
|
137
|
+
e: number;
|
|
138
|
+
f: number;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Transform the four corners of a LOCAL-space box (x0,y0)-(x1,y1) by a 2D matrix
|
|
142
|
+
* and return its axis-aligned bounding box in DEVICE space. Handles a general
|
|
143
|
+
* CTM (rotation / skew), not just translate+scale — a rotated rect maps to the
|
|
144
|
+
* AABB enclosing all four rotated corners. `pad` (px, device space) is added on
|
|
145
|
+
* every side so a blanked region fully covers antialiased glyph/stroke edges.
|
|
146
|
+
* Returns null when the inputs are non-finite (never throws into the hot path).
|
|
147
|
+
*/
|
|
148
|
+
export declare function transformedBox(x0: number, y0: number, x1: number, y1: number, m: Mat2D, pad?: number): MaskRegion | null;
|
|
149
|
+
/** Device-space AABB of a local-space rect (x,y,w,h) under transform `m`. */
|
|
150
|
+
export declare function rectRegion(x: number, y: number, w: number, h: number, m: Mat2D): MaskRegion | null;
|
|
151
|
+
/** Inputs to localize a drawn-text box: measured width + ascent/descent (from
|
|
152
|
+
* `measureText`), the draw origin (x,y), and the active `textAlign`/`textBaseline`
|
|
153
|
+
* which shift the box relative to (x,y). */
|
|
154
|
+
export interface TextBoxInput {
|
|
155
|
+
width: number;
|
|
156
|
+
ascent: number;
|
|
157
|
+
descent: number;
|
|
158
|
+
x: number;
|
|
159
|
+
y: number;
|
|
160
|
+
align?: string;
|
|
161
|
+
baseline?: string;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Compute a drawn-text bounding box in LOCAL (pre-transform) space from its
|
|
165
|
+
* measured metrics + alignment. textAlign shifts the box horizontally relative
|
|
166
|
+
* to the draw x (center → centered, right/end → left of x); textBaseline shifts
|
|
167
|
+
* it vertically relative to the draw y (top → below y, middle → centered,
|
|
168
|
+
* bottom → above y, default alphabetic → ascent above / descent below). The
|
|
169
|
+
* caller transforms this box to device space via `transformedBox`. start/end are
|
|
170
|
+
* treated as left/right (ltr assumption) — over-covering on rtl is harmless.
|
|
171
|
+
*/
|
|
172
|
+
export declare function textLocalBox(i: TextBoxInput): {
|
|
173
|
+
x0: number;
|
|
174
|
+
y0: number;
|
|
175
|
+
x1: number;
|
|
176
|
+
y1: number;
|
|
177
|
+
};
|
|
178
|
+
/** Device-space AABB of a drawn-text box — measure (text-local) then transform.
|
|
179
|
+
* `pad` (device px) should be font-size-proportional at the call site: the box is
|
|
180
|
+
* built from the glyphs' ADVANCE width, but italic / script / emoji ink overhangs
|
|
181
|
+
* the advance (side bearings), so a 1px pad would under-cover and leak a sliver on
|
|
182
|
+
* the keyframe. Over-covering is privacy-safe. */
|
|
183
|
+
export declare function textRegion(i: TextBoxInput, m: Mat2D, pad?: number): MaskRegion | null;
|
|
184
|
+
export type { SensitiveContentFlags };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compact binary encoding for the canvas command stream — verbatim port of the
|
|
3
|
+
* validated bench codec (test-applications/canvas-recorder-bench/src/capture/codec.ts).
|
|
4
|
+
* Opcode table + varint/f64 args + string table, deflate-compressed in ~1s batches.
|
|
5
|
+
*
|
|
6
|
+
* Op timestamps are EPOCH MS (contract §1); the µs-delta encoding below
|
|
7
|
+
* handles the large first delta (deltas are between ROUNDED absolute µs
|
|
8
|
+
* values so rounding error never accumulates).
|
|
9
|
+
*/
|
|
10
|
+
import type { RecordedOp } from "./protocol";
|
|
11
|
+
/** Stable opcode table: union of 2D methods, tracked props, and meta ops. */
|
|
12
|
+
export declare const OPCODES: string[];
|
|
13
|
+
export declare class CommandEncoder {
|
|
14
|
+
private w;
|
|
15
|
+
private strings;
|
|
16
|
+
private lastTUs;
|
|
17
|
+
/** count of ops in current batch */
|
|
18
|
+
ops: number;
|
|
19
|
+
encode(op: RecordedOp): void;
|
|
20
|
+
private str;
|
|
21
|
+
private arg;
|
|
22
|
+
/** Flush current batch as raw (uncompressed) bytes. */
|
|
23
|
+
takeBatch(): Uint8Array;
|
|
24
|
+
}
|
|
25
|
+
export declare function decodeCommands(batches: Uint8Array[]): RecordedOp[];
|
|
26
|
+
export declare function deflate(data: Uint8Array): Promise<Uint8Array>;
|
|
27
|
+
export declare function inflate(data: Uint8Array): Promise<Uint8Array>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A30 — device-class budgets. Canvas capture budgets/thresholds were sized for
|
|
3
|
+
* desktop; on a memory- or CPU-constrained mobile device the same budgets risk
|
|
4
|
+
* OOM / jank. This resolves a device class from `navigator` (hardware hints +
|
|
5
|
+
* UA) and returns scaled budgets: per-session byte budget, concurrent hardware
|
|
6
|
+
* video-encoder cap, governor demotion thresholds, and an iOS-Safari preference
|
|
7
|
+
* for the MediaRecorder path (WebCodecs is flaky/absent on older iOS).
|
|
8
|
+
*
|
|
9
|
+
* SSR rule: NO `navigator` access at module scope. `resolveDeviceBudgets()`
|
|
10
|
+
* reads the passed-in nav (tests) or the global `navigator` ONLY when present,
|
|
11
|
+
* and falls back to the desktop profile when neither exists (server render).
|
|
12
|
+
*/
|
|
13
|
+
import type { GovernorThresholds } from "./governor";
|
|
14
|
+
export type DeviceClass = "desktop" | "mobile" | "mobile-lowend";
|
|
15
|
+
export interface DeviceBudgets {
|
|
16
|
+
deviceClass: DeviceClass;
|
|
17
|
+
/** per-session canvas byte budget (SDK-side; backend has its own backstop) */
|
|
18
|
+
sessionByteBudget: number;
|
|
19
|
+
/** max concurrent hardware video encoders for this session */
|
|
20
|
+
maxConcurrentVideo: number;
|
|
21
|
+
/** governor demotion-threshold overrides (tighter on constrained devices) */
|
|
22
|
+
governor: Partial<GovernorThresholds>;
|
|
23
|
+
/** prefer the MediaRecorder backend over WebCodecs (iOS-Safari WebCodecs is
|
|
24
|
+
* unreliable / absent on older versions) */
|
|
25
|
+
preferMediaRecorder: boolean;
|
|
26
|
+
}
|
|
27
|
+
/** Desktop default — preserves the pre-A30 budgets exactly. */
|
|
28
|
+
export declare const DESKTOP_BUDGETS: DeviceBudgets;
|
|
29
|
+
/** The subset of `navigator` we read — all fields optional / best-effort. */
|
|
30
|
+
export interface NavigatorHints {
|
|
31
|
+
userAgent?: string;
|
|
32
|
+
deviceMemory?: number;
|
|
33
|
+
hardwareConcurrency?: number;
|
|
34
|
+
maxTouchPoints?: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Resolve device budgets. Pass `nav` in tests; in the browser it defaults to the
|
|
38
|
+
* global `navigator`; under SSR (no navigator) it returns the desktop profile.
|
|
39
|
+
*/
|
|
40
|
+
export declare function resolveDeviceBudgets(nav?: NavigatorHints): DeviceBudgets;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline-blob encode workers (webp image encoder + WebCodecs video encoder)
|
|
3
|
+
* and their promise/event clients. Ports of the bench workers
|
|
4
|
+
* (test-applications/canvas-recorder-bench/src/workers/{imgEncoder,videoEncoder}.worker.ts)
|
|
5
|
+
* as TEMPLATE-STRING sources so the vite lib build emits NO separate worker
|
|
6
|
+
* files — same precedent as WS_WORKER_CODE in src/websocket.tsx. Unlike the
|
|
7
|
+
* WS worker, this code may use modern JS: every browser with WebCodecs /
|
|
8
|
+
* OffscreenCanvas supports ES2020.
|
|
9
|
+
*
|
|
10
|
+
* SDK adaptation to the bench video worker: encoded chunk DATA is posted
|
|
11
|
+
* LIVE (transferred) instead of accumulated until stop, and the decoder
|
|
12
|
+
* config is posted once as `decoder-config` — the manager streams video-chunk
|
|
13
|
+
* / video-init SFCV frames as they are produced.
|
|
14
|
+
*
|
|
15
|
+
* SSR rule: workers are created lazily inside functions, never at module scope.
|
|
16
|
+
*/
|
|
17
|
+
export interface EncodedImage {
|
|
18
|
+
size: number;
|
|
19
|
+
bytes?: ArrayBuffer;
|
|
20
|
+
}
|
|
21
|
+
/** Encode a bitmap to webp in the worker. TRANSFERS (detaches) the bitmap. */
|
|
22
|
+
export declare function encodeBitmap(bitmap: ImageBitmap, quality: number, wantBytes?: boolean): Promise<EncodedImage>;
|
|
23
|
+
/** Tear down the shared image-encoder worker (manager stop path). */
|
|
24
|
+
export declare function terminateImgEncoder(): void;
|
|
25
|
+
export interface VideoEncoderEvents {
|
|
26
|
+
onDecoderConfig(codec: string, description: ArrayBuffer | null): void;
|
|
27
|
+
onChunk(data: ArrayBuffer, timestampUs: number, key: boolean): void;
|
|
28
|
+
onError(message: string): void;
|
|
29
|
+
}
|
|
30
|
+
export declare class VideoEncoderClient {
|
|
31
|
+
private events;
|
|
32
|
+
private worker;
|
|
33
|
+
private configured;
|
|
34
|
+
private doneResolve;
|
|
35
|
+
private flushResolve;
|
|
36
|
+
private configResolve;
|
|
37
|
+
constructor(events: VideoEncoderEvents);
|
|
38
|
+
get isConfigured(): boolean;
|
|
39
|
+
configure(cfg: {
|
|
40
|
+
width: number;
|
|
41
|
+
height: number;
|
|
42
|
+
bitrate: number;
|
|
43
|
+
codecs: string[];
|
|
44
|
+
}): Promise<boolean>;
|
|
45
|
+
/** TRANSFERS (closes) the frame. */
|
|
46
|
+
encodeFrame(frame: VideoFrame, key: boolean): void;
|
|
47
|
+
/** Non-terminal drain: flush queued frames out as chunks WITHOUT closing the
|
|
48
|
+
* encoder, so capture can resume. Resolves once the worker acks (or times
|
|
49
|
+
* out). Safe to call repeatedly. */
|
|
50
|
+
flush(timeoutMs?: number): Promise<void>;
|
|
51
|
+
/** Flush the encoder; resolves once all chunks have been delivered. */
|
|
52
|
+
stop(timeoutMs?: number): Promise<void>;
|
|
53
|
+
terminate(): void;
|
|
54
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 2 live-demotion governor (contract §5): per-canvas counters that
|
|
3
|
+
* decide when a command stream is too heavy and the canvas should demote to
|
|
4
|
+
* video capture.
|
|
5
|
+
*
|
|
6
|
+
* - POST-deflate command bytes: demote when the trailing 3s window carries
|
|
7
|
+
* more than 512 KB/s (i.e. > 1536 KB total in the window).
|
|
8
|
+
* - dynamic VIDEO-source drawImage: demote when more than 2 video-element
|
|
9
|
+
* draws land within any trailing 1s window.
|
|
10
|
+
* - ops per frame: demote when a single frame (ops sharing one timestamp)
|
|
11
|
+
* exceeds 5000 ops.
|
|
12
|
+
*/
|
|
13
|
+
export interface GovernorThresholds {
|
|
14
|
+
/** post-deflate command bytes per second (default 512 KiB/s) */
|
|
15
|
+
maxBytesPerSec: number;
|
|
16
|
+
/** how long the byte rate must be sustained (default 3000 ms) */
|
|
17
|
+
sustainMs: number;
|
|
18
|
+
/** video-element drawImage calls per second (default 2/s) */
|
|
19
|
+
maxVideoDrawsPerSec: number;
|
|
20
|
+
/** ops within a single frame timestamp (default 5000) */
|
|
21
|
+
maxOpsPerFrame: number;
|
|
22
|
+
}
|
|
23
|
+
export declare const DEFAULT_GOVERNOR_THRESHOLDS: GovernorThresholds;
|
|
24
|
+
export type DemoteReason = "bytes" | "video-source" | "ops";
|
|
25
|
+
export declare class CanvasGovernor {
|
|
26
|
+
private now;
|
|
27
|
+
private thresholds;
|
|
28
|
+
private byteSamples;
|
|
29
|
+
private videoDraws;
|
|
30
|
+
private frameT;
|
|
31
|
+
private frameOps;
|
|
32
|
+
private opsExceeded;
|
|
33
|
+
constructor(now: () => number, thresholds?: Partial<GovernorThresholds>);
|
|
34
|
+
/** Record post-deflate bytes for a flushed command batch. */
|
|
35
|
+
addBytes(n: number): void;
|
|
36
|
+
/** Record one dynamic video-source drawImage capture. */
|
|
37
|
+
addVideoDraw(): void;
|
|
38
|
+
/** Record one recorded op (tMs = the op's frame timestamp). */
|
|
39
|
+
addOp(tMs: number): void;
|
|
40
|
+
/** Returns the demotion reason once a threshold trips, else null. */
|
|
41
|
+
shouldDemote(): DemoteReason | null;
|
|
42
|
+
private pruneBytes;
|
|
43
|
+
private pruneVideo;
|
|
44
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Periodic pixel keyframes captured same-task — port of the bench hybrid
|
|
3
|
+
* strategy's takeKeyframe (test-applications/canvas-recorder-bench/src/
|
|
4
|
+
* capture/hybrid.ts). Keyframes are the bit-exact ground truth and the seek
|
|
5
|
+
* anchor; for 2D they also enable replay-from-keyframe (state snapshot rides
|
|
6
|
+
* the SFCV keyframe meta).
|
|
7
|
+
*/
|
|
8
|
+
import { type MaskRegion } from "./canvasPrivacy";
|
|
9
|
+
import type { StateSnapshot } from "./protocol";
|
|
10
|
+
export interface KeyframeOut {
|
|
11
|
+
tMs: number;
|
|
12
|
+
w: number;
|
|
13
|
+
h: number;
|
|
14
|
+
/** 2D shadow state (null for gl/gpu/worker sources) */
|
|
15
|
+
state: StateSnapshot | null;
|
|
16
|
+
/** webp bytes */
|
|
17
|
+
bytes: ArrayBuffer;
|
|
18
|
+
}
|
|
19
|
+
export declare class KeyframeCapturer {
|
|
20
|
+
private canvas;
|
|
21
|
+
private snapshotState;
|
|
22
|
+
private emit;
|
|
23
|
+
private note;
|
|
24
|
+
/**
|
|
25
|
+
* Masked-region source (maskCanvasDraws / redact-mode text). Each device-space
|
|
26
|
+
* bbox is filled with REDACTION_COLOR on the scratch copy BEFORE the keyframe
|
|
27
|
+
* is encoded, so the real pixels of masked content never ship — replay then
|
|
28
|
+
* draws the baked redaction (recolored shapes / asterisks / bars) from the
|
|
29
|
+
* command stream on top of the blanked keyframe. Defaults to none (GL/other
|
|
30
|
+
* sources have no per-op masking).
|
|
31
|
+
*/
|
|
32
|
+
private getMaskedRegions;
|
|
33
|
+
private scratch;
|
|
34
|
+
private scratchCtx;
|
|
35
|
+
private lastKeyframe;
|
|
36
|
+
/** has the canvas actually rendered since the last keyframe? */
|
|
37
|
+
private dirtySinceKeyframe;
|
|
38
|
+
private interactionRequested;
|
|
39
|
+
private pending;
|
|
40
|
+
private stopped;
|
|
41
|
+
constructor(canvas: HTMLCanvasElement, snapshotState: () => StateSnapshot | null, emit: (kf: KeyframeOut) => void, note: (msg: string) => void,
|
|
42
|
+
/**
|
|
43
|
+
* Masked-region source (maskCanvasDraws / redact-mode text). Each device-space
|
|
44
|
+
* bbox is filled with REDACTION_COLOR on the scratch copy BEFORE the keyframe
|
|
45
|
+
* is encoded, so the real pixels of masked content never ship — replay then
|
|
46
|
+
* draws the baked redaction (recolored shapes / asterisks / bars) from the
|
|
47
|
+
* command stream on top of the blanked keyframe. Defaults to none (GL/other
|
|
48
|
+
* sources have no per-op masking).
|
|
49
|
+
*/
|
|
50
|
+
getMaskedRegions?: () => readonly MaskRegion[]);
|
|
51
|
+
onInteraction(tMs: number): void;
|
|
52
|
+
/**
|
|
53
|
+
* A27 — one-shot keyframe for a canvas that was already painted BEFORE the
|
|
54
|
+
* SDK loaded and may never redraw (a static pre-existing canvas). The normal
|
|
55
|
+
* keyframe pacing is dirty-gated (afterRender only runs after a draw), so a
|
|
56
|
+
* canvas that is painted once and then idle would never emit a keyframe and
|
|
57
|
+
* would replay blank. This forces a single ground-truth keyframe of the
|
|
58
|
+
* canvas's current pixels, independent of the dirty flag or the interval.
|
|
59
|
+
*
|
|
60
|
+
* Unlike afterRender this is NOT same-task with a render — but a STATIC
|
|
61
|
+
* pre-existing 2D canvas's committed pixels persist outside any rendering
|
|
62
|
+
* task (the guardrail-#6 hazard only affects pdb:false WebGL / task-scoped
|
|
63
|
+
* WebGPU textures, which are not what A27 targets). takeKeyframe's
|
|
64
|
+
* drawImage(canvas) copy throws on a zero-size canvas (caught -> no emit) and
|
|
65
|
+
* a masked/tainted canvas never reaches here (the manager only builds a
|
|
66
|
+
* controller for unmasked canvases, and a tainted readback simply fails the
|
|
67
|
+
* encode). Returns true if a keyframe copy was kicked off.
|
|
68
|
+
*/
|
|
69
|
+
initialKeyframe(tMs: number): boolean;
|
|
70
|
+
/** Same-task keyframes: GPU-canvas-safe and never captures mid-draw. */
|
|
71
|
+
afterRender(tMs: number): void;
|
|
72
|
+
takeKeyframe(t: number): void;
|
|
73
|
+
/** Final keyframe (if dirty) + settle outstanding encodes. */
|
|
74
|
+
stop(finalT: number): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* A22 / A6: immediate disposal with NO final keyframe — used when the canvas
|
|
77
|
+
* was removed from the DOM or masked mid-session (its pixels are gone or must
|
|
78
|
+
* not be recorded). Releases the scratch keyframe canvas so it can be GC'd.
|
|
79
|
+
*/
|
|
80
|
+
dispose(): void;
|
|
81
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
export interface MediaRecorderCaptureOut {
|
|
31
|
+
/** decoder/container known — emit the SFCV video-init frame (no payload). */
|
|
32
|
+
onInit(mimeType: string, w: number, h: number): void;
|
|
33
|
+
/** one MediaRecorder Blob's bytes — emit an SFCV video-chunk frame. */
|
|
34
|
+
onChunk(seq: number, data: ArrayBuffer): void;
|
|
35
|
+
note(msg: string): void;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Feature detection (contract): MediaRecorder fallback is usable ONLY when
|
|
39
|
+
* `MediaRecorder`, `HTMLCanvasElement.prototype.captureStream`, and
|
|
40
|
+
* `MediaRecorder.isTypeSupported` for at least one candidate mime are present.
|
|
41
|
+
* Callers prefer WebCodecs and reach here only when WebCodecs is ABSENT.
|
|
42
|
+
*/
|
|
43
|
+
export declare function mediaRecorderSupported(): boolean;
|
|
44
|
+
/** First candidate mime MediaRecorder claims to support, or null. */
|
|
45
|
+
export declare function pickMime(): string | null;
|
|
46
|
+
export declare class MediaRecorderCapture {
|
|
47
|
+
private canvas;
|
|
48
|
+
private out;
|
|
49
|
+
private recorder;
|
|
50
|
+
private stream;
|
|
51
|
+
private stopped;
|
|
52
|
+
private started;
|
|
53
|
+
private seq;
|
|
54
|
+
/** chain Blob -> bytes emission so seq order is preserved across async reads */
|
|
55
|
+
private emitChain;
|
|
56
|
+
constructor(canvas: HTMLCanvasElement, out: MediaRecorderCaptureOut);
|
|
57
|
+
static supported(): boolean;
|
|
58
|
+
/** Resolves true once the recorder started; false -> keyframes-only fallback. */
|
|
59
|
+
start(): Promise<boolean>;
|
|
60
|
+
/** A non-empty Blob from dataavailable. Read bytes async, emit in seq order. */
|
|
61
|
+
private onData;
|
|
62
|
+
/**
|
|
63
|
+
* The manager's microtask pump calls this after every render for dirtiness
|
|
64
|
+
* bookkeeping. MediaRecorder pulls frames off the captureStream track on its
|
|
65
|
+
* own, so this is intentionally a no-op (kept for VideoCapture parity). The
|
|
66
|
+
* `_t` mirrors VideoCapture.afterRender's signature.
|
|
67
|
+
*/
|
|
68
|
+
afterRender(_t: number): void;
|
|
69
|
+
/** Interaction boost is a no-op: the captureStream fps is fixed and the
|
|
70
|
+
* recorder cannot be told to key on demand. Kept for VideoCapture parity. */
|
|
71
|
+
onInteraction(_tMs: number): void;
|
|
72
|
+
/**
|
|
73
|
+
* Non-terminal drain (visibilitychange:hidden): force the recorder to emit a
|
|
74
|
+
* chunk for everything buffered so far WITHOUT stopping, so capture resumes if
|
|
75
|
+
* the tab returns. Mirrors VideoCapture.flush(). No-op once stopped.
|
|
76
|
+
*/
|
|
77
|
+
flush(): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Terminal stop: flush the final chunk (requestData then stop fire a last
|
|
80
|
+
* dataavailable), settle the emission chain so the final bytes reach onChunk,
|
|
81
|
+
* then release the recorder + stream tracks. Idempotent.
|
|
82
|
+
*/
|
|
83
|
+
stop(): Promise<void>;
|
|
84
|
+
/**
|
|
85
|
+
* Immediate teardown WITHOUT flushing (A22/A6/A10 parity): the canvas is gone
|
|
86
|
+
* or must not be recorded, so drop the recorder and stop the stream tracks
|
|
87
|
+
* without emitting the tail. Idempotent.
|
|
88
|
+
*/
|
|
89
|
+
abort(): void;
|
|
90
|
+
private teardownStream;
|
|
91
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
export interface ObjectLog {
|
|
27
|
+
/** context factory name, or '__path2d' */
|
|
28
|
+
factory: string;
|
|
29
|
+
args: unknown[];
|
|
30
|
+
calls: {
|
|
31
|
+
method: string;
|
|
32
|
+
args: unknown[];
|
|
33
|
+
}[];
|
|
34
|
+
/** A26: set once the per-object calls[] cap was hit; further calls are
|
|
35
|
+
* dropped so a long-lived/reused gradient or Path2D cannot leak unbounded. */
|
|
36
|
+
callsCapped?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/** factory-created object -> its construction + method-call history */
|
|
39
|
+
export declare const objectLogs: WeakMap<object, ObjectLog>;
|
|
40
|
+
/**
|
|
41
|
+
* A26 — per-object recorded-calls cap. Gradients/patterns/Path2Ds are normally
|
|
42
|
+
* built with a handful of stops/segments at construction; a few apps, though,
|
|
43
|
+
* reuse one long-lived object and keep appending to it for the whole session
|
|
44
|
+
* (e.g. a Path2D that accumulates points), growing `calls[]` without bound and
|
|
45
|
+
* leaking memory. We cap the recorded history: the first MAX_OBJECT_CALLS calls
|
|
46
|
+
* are kept (covering every realistic construction), and anything beyond is
|
|
47
|
+
* dropped with a one-time marker. Replay of the common case is unaffected.
|
|
48
|
+
*/
|
|
49
|
+
export declare const MAX_OBJECT_CALLS = 4096;
|
|
50
|
+
/** Append a method call to an object's log, honoring the per-object cap. */
|
|
51
|
+
export declare function pushObjectCall(log: ObjectLog | undefined, method: string, args: unknown[]): void;
|
|
52
|
+
export declare const OBJECT_FACTORIES: readonly ["createLinearGradient", "createRadialGradient", "createConicGradient", "createPattern"];
|
|
53
|
+
export declare const PATH_OPS: readonly ["addPath", "arc", "arcTo", "bezierCurveTo", "closePath", "ellipse", "lineTo", "moveTo", "quadraticCurveTo", "rect", "roundRect"];
|
|
54
|
+
/** The unwrapped Path2D constructor (set once Path2D logging installs). */
|
|
55
|
+
export declare function getNativePath2D(): typeof Path2D | null;
|
|
56
|
+
/**
|
|
57
|
+
* Wrap the gradient/pattern factory methods of one 2D-context prototype
|
|
58
|
+
* (CanvasRenderingContext2D on the main thread, OffscreenCanvasRendering-
|
|
59
|
+
* Context2D inside workers) so every created object lands in `objectLogs`.
|
|
60
|
+
* Idempotent per prototype.
|
|
61
|
+
*/
|
|
62
|
+
export declare function installFactoryLoggingOnProto(protoObj: object): void;
|
|
63
|
+
/**
|
|
64
|
+
* Wrap the realm-global object types: CanvasGradient.addColorStop appends to
|
|
65
|
+
* the creating factory's log; Path2D construction + segment ops are logged
|
|
66
|
+
* per instance (replayed via __create '__path2d' + __objcall when a path is
|
|
67
|
+
* first used on a recorded context). Idempotent.
|
|
68
|
+
*/
|
|
69
|
+
export declare function installSharedObjectLogging(): void;
|
|
70
|
+
/**
|
|
71
|
+
* Early hook for the SDK entry path: arm all object logging on the main
|
|
72
|
+
* thread BEFORE app scene code constructs its gradients/patterns/paths.
|
|
73
|
+
* Safe to call multiple times; no-ops outside a browser realm.
|
|
74
|
+
*/
|
|
75
|
+
export declare function installEarlyObjectLogging(): void;
|