@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.
Files changed (144) hide show
  1. package/README.md +8 -0
  2. package/dist/canvas/adapters.js +382 -0
  3. package/dist/canvas/adapters.js.br +0 -0
  4. package/dist/canvas/adapters.js.gz +0 -0
  5. package/dist/canvas/canvasManager.js +1949 -0
  6. package/dist/canvas/canvasManager.js.br +0 -0
  7. package/dist/canvas/canvasManager.js.gz +0 -0
  8. package/dist/canvas/canvasPrivacy.js +233 -0
  9. package/dist/canvas/canvasPrivacy.js.br +0 -0
  10. package/dist/canvas/canvasPrivacy.js.gz +0 -0
  11. package/dist/canvas/codec.js +314 -0
  12. package/dist/canvas/codec.js.br +0 -0
  13. package/dist/canvas/codec.js.gz +0 -0
  14. package/dist/canvas/deviceClass.js +67 -0
  15. package/dist/canvas/deviceClass.js.br +0 -0
  16. package/dist/canvas/deviceClass.js.gz +0 -0
  17. package/dist/canvas/encodeWorker.js +284 -0
  18. package/dist/canvas/encodeWorker.js.br +0 -0
  19. package/dist/canvas/encodeWorker.js.gz +0 -0
  20. package/dist/canvas/governor.js +79 -0
  21. package/dist/canvas/governor.js.br +0 -0
  22. package/dist/canvas/governor.js.gz +0 -0
  23. package/dist/canvas/keyframes.js +183 -0
  24. package/dist/canvas/keyframes.js.br +0 -0
  25. package/dist/canvas/keyframes.js.gz +0 -0
  26. package/dist/canvas/mediaRecorderCapture.js +257 -0
  27. package/dist/canvas/mediaRecorderCapture.js.br +0 -0
  28. package/dist/canvas/mediaRecorderCapture.js.gz +0 -0
  29. package/dist/canvas/objectLog.js +150 -0
  30. package/dist/canvas/objectLog.js.br +0 -0
  31. package/dist/canvas/objectLog.js.gz +0 -0
  32. package/dist/canvas/patch2d.js +1302 -0
  33. package/dist/canvas/patch2d.js.br +0 -0
  34. package/dist/canvas/patch2d.js.gz +0 -0
  35. package/dist/canvas/patchGL.js +371 -0
  36. package/dist/canvas/patchGL.js.br +0 -0
  37. package/dist/canvas/patchGL.js.gz +0 -0
  38. package/dist/canvas/patchWebGPU.js +361 -0
  39. package/dist/canvas/patchWebGPU.js.br +0 -0
  40. package/dist/canvas/patchWebGPU.js.gz +0 -0
  41. package/dist/canvas/piiDetect.js +155 -0
  42. package/dist/canvas/piiDetect.js.br +0 -0
  43. package/dist/canvas/piiDetect.js.gz +0 -0
  44. package/dist/canvas/protocol.js +122 -0
  45. package/dist/canvas/protocol.js.br +0 -0
  46. package/dist/canvas/protocol.js.gz +0 -0
  47. package/dist/canvas/videoCapture.js +252 -0
  48. package/dist/canvas/videoCapture.js.br +0 -0
  49. package/dist/canvas/videoCapture.js.gz +0 -0
  50. package/dist/canvas/workerRelay.js +99 -0
  51. package/dist/canvas/workerRelay.js.br +0 -0
  52. package/dist/canvas/workerRelay.js.gz +0 -0
  53. package/dist/canvas/workerShim.js +227 -0
  54. package/dist/canvas/workerShim.js.br +0 -0
  55. package/dist/canvas/workerShim.js.gz +0 -0
  56. package/dist/canvas-worker.cjs +74 -0
  57. package/dist/canvas-worker.cjs.br +0 -0
  58. package/dist/canvas-worker.cjs.gz +0 -0
  59. package/dist/canvas-worker.js +78 -0
  60. package/dist/canvas-worker.js.br +0 -0
  61. package/dist/canvas-worker.js.gz +0 -0
  62. package/dist/canvasFrameStore.js +198 -0
  63. package/dist/canvasFrameStore.js.br +0 -0
  64. package/dist/canvasFrameStore.js.gz +0 -0
  65. package/dist/chunks/canvasManager-BYn3_oyo.js +1440 -0
  66. package/dist/chunks/canvasManager-BYn3_oyo.js.br +0 -0
  67. package/dist/chunks/canvasManager-BYn3_oyo.js.gz +0 -0
  68. package/dist/chunks/canvasManager-CoX5072G.js +1456 -0
  69. package/dist/chunks/canvasManager-CoX5072G.js.br +0 -0
  70. package/dist/chunks/canvasManager-CoX5072G.js.gz +0 -0
  71. package/dist/chunks/{chunkSerializer-FQtY90Av.js → chunkSerializer-C_K0Wyk0.js} +1 -1
  72. package/dist/chunks/chunkSerializer-C_K0Wyk0.js.br +0 -0
  73. package/dist/chunks/chunkSerializer-C_K0Wyk0.js.gz +0 -0
  74. package/dist/chunks/{chunkSerializer-DDukZpgl.js → chunkSerializer-CghPitF4.js} +1 -1
  75. package/dist/chunks/chunkSerializer-CghPitF4.js.br +0 -0
  76. package/dist/chunks/chunkSerializer-CghPitF4.js.gz +0 -0
  77. package/dist/chunks/{index-C-qbsfKe.js → index-BIj4l3CV.js} +1028 -650
  78. package/dist/chunks/index-BIj4l3CV.js.br +0 -0
  79. package/dist/chunks/index-BIj4l3CV.js.gz +0 -0
  80. package/dist/chunks/{index-D6axlCRu.js → index-DIYomvdD.js} +1119 -720
  81. package/dist/chunks/index-DIYomvdD.js.br +0 -0
  82. package/dist/chunks/index-DIYomvdD.js.gz +0 -0
  83. package/dist/chunks/patch2d-9voowhJ_.js +816 -0
  84. package/dist/chunks/patch2d-9voowhJ_.js.br +0 -0
  85. package/dist/chunks/patch2d-9voowhJ_.js.gz +0 -0
  86. package/dist/chunks/patch2d-PQxcaE6r.js +886 -0
  87. package/dist/chunks/patch2d-PQxcaE6r.js.br +0 -0
  88. package/dist/chunks/patch2d-PQxcaE6r.js.gz +0 -0
  89. package/dist/graphql.js +12 -1
  90. package/dist/graphql.js.br +0 -0
  91. package/dist/graphql.js.gz +0 -0
  92. package/dist/inAppReportIssueModal/index.js +48 -0
  93. package/dist/inAppReportIssueModal/index.js.br +0 -0
  94. package/dist/inAppReportIssueModal/index.js.gz +0 -0
  95. package/dist/index.js +144 -2
  96. package/dist/index.js.br +0 -0
  97. package/dist/index.js.gz +0 -0
  98. package/dist/recorder.cjs +2 -2
  99. package/dist/recorder.cjs.br +0 -0
  100. package/dist/recorder.cjs.gz +0 -0
  101. package/dist/recorder.js +52 -41
  102. package/dist/recorder.js.br +0 -0
  103. package/dist/recorder.js.gz +0 -0
  104. package/dist/recorder.umd.cjs +4847 -2212
  105. package/dist/recorder.umd.cjs.br +0 -0
  106. package/dist/recorder.umd.cjs.gz +0 -0
  107. package/dist/recording.js +119 -10
  108. package/dist/recording.js.br +0 -0
  109. package/dist/recording.js.gz +0 -0
  110. package/dist/types/canvas/adapters.d.ts +71 -0
  111. package/dist/types/canvas/canvasManager.d.ts +273 -0
  112. package/dist/types/canvas/canvasPrivacy.d.ts +184 -0
  113. package/dist/types/canvas/codec.d.ts +27 -0
  114. package/dist/types/canvas/deviceClass.d.ts +40 -0
  115. package/dist/types/canvas/encodeWorker.d.ts +54 -0
  116. package/dist/types/canvas/governor.d.ts +44 -0
  117. package/dist/types/canvas/keyframes.d.ts +81 -0
  118. package/dist/types/canvas/mediaRecorderCapture.d.ts +91 -0
  119. package/dist/types/canvas/objectLog.d.ts +75 -0
  120. package/dist/types/canvas/patch2d.d.ts +339 -0
  121. package/dist/types/canvas/patchGL.d.ts +74 -0
  122. package/dist/types/canvas/patchWebGPU.d.ts +37 -0
  123. package/dist/types/canvas/piiDetect.d.ts +60 -0
  124. package/dist/types/canvas/protocol.d.ts +176 -0
  125. package/dist/types/canvas/videoCapture.d.ts +67 -0
  126. package/dist/types/canvas/workerRelay.d.ts +29 -0
  127. package/dist/types/canvas/workerShim.d.ts +10 -0
  128. package/dist/types/canvasFrameStore.d.ts +23 -0
  129. package/dist/types/graphql.d.ts +1 -1
  130. package/dist/types/index.d.ts +99 -1
  131. package/dist/types/types.d.ts +40 -0
  132. package/dist/types/websocket.d.ts +19 -0
  133. package/dist/websocket.js +210 -0
  134. package/dist/websocket.js.br +0 -0
  135. package/dist/websocket.js.gz +0 -0
  136. package/package.json +7 -1
  137. package/dist/chunks/chunkSerializer-DDukZpgl.js.br +0 -0
  138. package/dist/chunks/chunkSerializer-DDukZpgl.js.gz +0 -0
  139. package/dist/chunks/chunkSerializer-FQtY90Av.js.br +0 -0
  140. package/dist/chunks/chunkSerializer-FQtY90Av.js.gz +0 -0
  141. package/dist/chunks/index-C-qbsfKe.js.br +0 -0
  142. package/dist/chunks/index-C-qbsfKe.js.gz +0 -0
  143. package/dist/chunks/index-D6axlCRu.js.br +0 -0
  144. 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;