Skip to content

Commit 6d12350

Browse files
c298leebillyvg
authored andcommitted
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 f8a2c9f commit 6d12350

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,7 +333,7 @@ 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

@@ -242,7 +343,10 @@ export class CanvasManager implements CanvasManagerInterface {
242343
if (canvas.width === 0 || canvas.height === 0) return;
243344

244345
snapshotInProgressMap.set(id, true);
245-
if (['webgl', 'webgl2'].includes((canvas as ICanvas).__context)) {
346+
if (
347+
!isManualSnapshot &&
348+
['webgl', 'webgl2'].includes((canvas as ICanvas).__context)
349+
) {
246350
// if the canvas hasn't been modified recently,
247351
// its contents won't be in memory and `createImageBitmap`
248352
// will return a transparent imageBitmap
@@ -273,7 +377,7 @@ export class CanvasManager implements CanvasManagerInterface {
273377
bitmap,
274378
width: canvas.width,
275379
height: canvas.height,
276-
dataURLOptions: options.dataURLOptions,
380+
dataURLOptions,
277381
},
278382
[bitmap],
279383
);
@@ -288,51 +392,7 @@ export class CanvasManager implements CanvasManagerInterface {
288392
};
289393

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

338398
private startPendingCanvasMutationFlusher() {

0 commit comments

Comments
 (0)