Skip to content

Commit 2edc45b

Browse files
authored
feat(replays): Add manual canvas snapshot function (#149)
Adds a snapshot canvas function that allows you to manually snapshot canvas elements, which enables recording of 3d and webgl canvas Requires getsentry/sentry-javascript#10066
1 parent 2fa46f3 commit 2edc45b

File tree

1 file changed

+109
-49
lines changed

1 file changed

+109
-49
lines changed

packages/rrweb/src/record/observers/canvas/canvas-manager.ts

Lines changed: 109 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,12 @@ export interface CanvasManagerInterface {
3535
unfreeze(): void;
3636
lock(): void;
3737
unlock(): void;
38+
snapshot(canvasElement?: HTMLCanvasElement): void;
3839
}
3940

4041
export interface CanvasManagerConstructorOptions {
4142
recordCanvas: boolean;
43+
isManualSnapshot?: boolean;
4244
mutationCb: canvasMutationCallback;
4345
win: IWindow;
4446
blockClass: blockClass;
@@ -65,11 +67,15 @@ export class CanvasManagerNoop implements CanvasManagerInterface {
6567
public unlock() {
6668
// noop
6769
}
70+
public snapshot() {
71+
// noop
72+
}
6873
}
6974

7075
export class CanvasManager implements CanvasManagerInterface {
7176
private pendingCanvasMutations: pendingCanvasMutationsMap = new Map();
7277
private rafStamps: RafStamps = { latestId: 0, invokeId: null };
78+
private options: CanvasManagerConstructorOptions;
7379
private mirror: Mirror;
7480

7581
private mutationCb: canvasMutationCallback;
@@ -110,6 +116,11 @@ export class CanvasManager implements CanvasManagerInterface {
110116
} = options;
111117
this.mutationCb = options.mutationCb;
112118
this.mirror = options.mirror;
119+
this.options = options;
120+
121+
if (options.isManualSnapshot) {
122+
return;
123+
}
113124

114125
callbackWrapper(() => {
115126
if (recordCanvas && sampling === 'all')
@@ -167,6 +178,90 @@ export class CanvasManager implements CanvasManagerInterface {
167178
unblockSelector,
168179
true,
169180
);
181+
const rafId = this.takeSnapshot(
182+
false,
183+
fps,
184+
win,
185+
blockClass,
186+
blockSelector,
187+
unblockSelector,
188+
options.dataURLOptions,
189+
);
190+
191+
this.resetObservers = () => {
192+
canvasContextReset();
193+
cancelAnimationFrame(rafId);
194+
};
195+
}
196+
197+
private initCanvasMutationObserver(
198+
win: IWindow,
199+
blockClass: blockClass,
200+
blockSelector: string | null,
201+
unblockSelector: string | null,
202+
): void {
203+
this.startRAFTimestamping();
204+
this.startPendingCanvasMutationFlusher();
205+
206+
const canvasContextReset = initCanvasContextObserver(
207+
win,
208+
blockClass,
209+
blockSelector,
210+
unblockSelector,
211+
false,
212+
);
213+
const canvas2DReset = initCanvas2DMutationObserver(
214+
this.processMutation.bind(this),
215+
win,
216+
blockClass,
217+
blockSelector,
218+
unblockSelector,
219+
);
220+
221+
const canvasWebGL1and2Reset = initCanvasWebGLMutationObserver(
222+
this.processMutation.bind(this),
223+
win,
224+
blockClass,
225+
blockSelector,
226+
unblockSelector,
227+
this.mirror,
228+
);
229+
230+
this.resetObservers = () => {
231+
canvasContextReset();
232+
canvas2DReset();
233+
canvasWebGL1and2Reset();
234+
};
235+
}
236+
237+
public snapshot(canvasElement?: HTMLCanvasElement) {
238+
const { options } = this;
239+
const rafId = this.takeSnapshot(
240+
true,
241+
options.sampling === 'all' ? 2 : options.sampling || 2,
242+
options.win,
243+
options.blockClass,
244+
options.blockSelector,
245+
options.unblockSelector,
246+
options.dataURLOptions,
247+
canvasElement,
248+
);
249+
250+
this.resetObservers = () => {
251+
cancelAnimationFrame(rafId);
252+
};
253+
}
254+
255+
private takeSnapshot(
256+
isManualSnapshot: boolean,
257+
fps: number,
258+
win: IWindow,
259+
blockClass: blockClass,
260+
blockSelector: string | null,
261+
unblockSelector: string | null,
262+
dataURLOptions: DataURLOptions,
263+
canvasElement?: HTMLCanvasElement,
264+
) {
170265
const snapshotInProgressMap: Map<number, boolean> = new Map();
171266
const worker = new Worker(getImageBitmapDataUrlWorkerURL());
172267
worker.onmessage = (e) => {
@@ -210,7 +305,13 @@ export class CanvasManager implements CanvasManagerInterface {
210305
let lastSnapshotTime = 0;
211306
let rafId: number;
212307

213-
const getCanvas = (): HTMLCanvasElement[] => {
308+
const getCanvas = (
309+
canvasElement?: HTMLCanvasElement,
310+
): HTMLCanvasElement[] => {
311+
if (canvasElement) {
312+
return [canvasElement];
313+
}
314+
214315
const matchedCanvas: HTMLCanvasElement[] = [];
215316
win.document.querySelectorAll('canvas').forEach((canvas) => {
216317
if (
@@ -232,11 +333,14 @@ export class CanvasManager implements CanvasManagerInterface {
232333
}
233334
lastSnapshotTime = timestamp;
234335

235-
getCanvas().forEach((canvas: HTMLCanvasElement) => {
336+
getCanvas(canvasElement).forEach((canvas: HTMLCanvasElement) => {
236337
const id = this.mirror.getId(canvas);
237338
if (snapshotInProgressMap.get(id)) return;
238339
snapshotInProgressMap.set(id, true);
239-
if (['webgl', 'webgl2'].includes((canvas as ICanvas).__context)) {
340+
if (
341+
!isManualSnapshot &&
342+
['webgl', 'webgl2'].includes((canvas as ICanvas).__context)
343+
) {
240344
// if the canvas hasn't been modified recently,
241345
// its contents won't be in memory and `createImageBitmap`
242346
// will return a transparent imageBitmap
@@ -267,7 +371,7 @@ export class CanvasManager implements CanvasManagerInterface {
267371
bitmap,
268372
width: canvas.width,
269373
height: canvas.height,
270-
dataURLOptions: options.dataURLOptions,
374+
dataURLOptions,
271375
},
272376
[bitmap],
273377
);
@@ -282,51 +386,7 @@ export class CanvasManager implements CanvasManagerInterface {
282386
};
283387

284388
rafId = onRequestAnimationFrame(takeCanvasSnapshots);
285-
286-
this.resetObservers = () => {
287-
canvasContextReset();
288-
cancelAnimationFrame(rafId);
289-
};
290-
}
291-
292-
private initCanvasMutationObserver(
293-
win: IWindow,
294-
blockClass: blockClass,
295-
blockSelector: string | null,
296-
unblockSelector: string | null,
297-
): void {
298-
this.startRAFTimestamping();
299-
this.startPendingCanvasMutationFlusher();
300-
301-
const canvasContextReset = initCanvasContextObserver(
302-
win,
303-
blockClass,
304-
blockSelector,
305-
unblockSelector,
306-
false,
307-
);
308-
const canvas2DReset = initCanvas2DMutationObserver(
309-
this.processMutation.bind(this),
310-
win,
311-
blockClass,
312-
blockSelector,
313-
unblockSelector,
314-
);
315-
316-
const canvasWebGL1and2Reset = initCanvasWebGLMutationObserver(
317-
this.processMutation.bind(this),
318-
win,
319-
blockClass,
320-
blockSelector,
321-
unblockSelector,
322-
this.mirror,
323-
);
324-
325-
this.resetObservers = () => {
326-
canvasContextReset();
327-
canvas2DReset();
328-
canvasWebGL1and2Reset();
329-
};
389+
return rafId;
330390
}
331391

332392
private startPendingCanvasMutationFlusher() {

0 commit comments

Comments
 (0)