Skip to content

Commit

Permalink
feat(replays): Add manual canvas snapshot function (#149)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
c298lee authored and billyvg committed Apr 26, 2024
1 parent f8a2c9f commit 6d12350
Showing 1 changed file with 109 additions and 49 deletions.
158 changes: 109 additions & 49 deletions packages/rrweb/src/record/observers/canvas/canvas-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@ export interface CanvasManagerInterface {
unfreeze(): void;
lock(): void;
unlock(): void;
snapshot(canvasElement?: HTMLCanvasElement): void;
}

export interface CanvasManagerConstructorOptions {
recordCanvas: boolean;
isManualSnapshot?: boolean;
mutationCb: canvasMutationCallback;
win: IWindow;
blockClass: blockClass;
Expand All @@ -65,11 +67,15 @@ export class CanvasManagerNoop implements CanvasManagerInterface {
public unlock() {
// noop
}
public snapshot() {
// noop
}
}

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

private mutationCb: canvasMutationCallback;
Expand Down Expand Up @@ -110,6 +116,11 @@ export class CanvasManager implements CanvasManagerInterface {
} = options;
this.mutationCb = options.mutationCb;
this.mirror = options.mirror;
this.options = options;

if (options.isManualSnapshot) {
return;
}

callbackWrapper(() => {
if (recordCanvas && sampling === 'all')
Expand Down Expand Up @@ -167,6 +178,90 @@ export class CanvasManager implements CanvasManagerInterface {
unblockSelector,
true,
);
const rafId = this.takeSnapshot(
false,
fps,
win,
blockClass,
blockSelector,
unblockSelector,
options.dataURLOptions,
);

this.resetObservers = () => {
canvasContextReset();
cancelAnimationFrame(rafId);
};
}

private initCanvasMutationObserver(
win: IWindow,
blockClass: blockClass,
blockSelector: string | null,
unblockSelector: string | null,
): void {
this.startRAFTimestamping();
this.startPendingCanvasMutationFlusher();

const canvasContextReset = initCanvasContextObserver(
win,
blockClass,
blockSelector,
unblockSelector,
false,
);
const canvas2DReset = initCanvas2DMutationObserver(
this.processMutation.bind(this),
win,
blockClass,
blockSelector,
unblockSelector,
);

const canvasWebGL1and2Reset = initCanvasWebGLMutationObserver(
this.processMutation.bind(this),
win,
blockClass,
blockSelector,
unblockSelector,
this.mirror,
);

this.resetObservers = () => {
canvasContextReset();
canvas2DReset();
canvasWebGL1and2Reset();
};
}

public snapshot(canvasElement?: HTMLCanvasElement) {
const { options } = this;
const rafId = this.takeSnapshot(
true,
options.sampling === 'all' ? 2 : options.sampling || 2,
options.win,
options.blockClass,
options.blockSelector,
options.unblockSelector,
options.dataURLOptions,
canvasElement,
);

this.resetObservers = () => {
cancelAnimationFrame(rafId);
};
}

private takeSnapshot(
isManualSnapshot: boolean,
fps: number,
win: IWindow,
blockClass: blockClass,
blockSelector: string | null,
unblockSelector: string | null,
dataURLOptions: DataURLOptions,
canvasElement?: HTMLCanvasElement,
) {
const snapshotInProgressMap: Map<number, boolean> = new Map();
const worker = new Worker(getImageBitmapDataUrlWorkerURL());
worker.onmessage = (e) => {
Expand Down Expand Up @@ -210,7 +305,13 @@ export class CanvasManager implements CanvasManagerInterface {
let lastSnapshotTime = 0;
let rafId: number;

const getCanvas = (): HTMLCanvasElement[] => {
const getCanvas = (
canvasElement?: HTMLCanvasElement,
): HTMLCanvasElement[] => {
if (canvasElement) {
return [canvasElement];
}

const matchedCanvas: HTMLCanvasElement[] = [];
win.document.querySelectorAll('canvas').forEach((canvas) => {
if (
Expand All @@ -232,7 +333,7 @@ export class CanvasManager implements CanvasManagerInterface {
}
lastSnapshotTime = timestamp;

getCanvas().forEach((canvas: HTMLCanvasElement) => {
getCanvas(canvasElement).forEach((canvas: HTMLCanvasElement) => {
const id = this.mirror.getId(canvas);
if (snapshotInProgressMap.get(id)) return;

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

snapshotInProgressMap.set(id, true);
if (['webgl', 'webgl2'].includes((canvas as ICanvas).__context)) {
if (
!isManualSnapshot &&
['webgl', 'webgl2'].includes((canvas as ICanvas).__context)
) {
// if the canvas hasn't been modified recently,
// its contents won't be in memory and `createImageBitmap`
// will return a transparent imageBitmap
Expand Down Expand Up @@ -273,7 +377,7 @@ export class CanvasManager implements CanvasManagerInterface {
bitmap,
width: canvas.width,
height: canvas.height,
dataURLOptions: options.dataURLOptions,
dataURLOptions,
},
[bitmap],
);
Expand All @@ -288,51 +392,7 @@ export class CanvasManager implements CanvasManagerInterface {
};

rafId = onRequestAnimationFrame(takeCanvasSnapshots);

this.resetObservers = () => {
canvasContextReset();
cancelAnimationFrame(rafId);
};
}

private initCanvasMutationObserver(
win: IWindow,
blockClass: blockClass,
blockSelector: string | null,
unblockSelector: string | null,
): void {
this.startRAFTimestamping();
this.startPendingCanvasMutationFlusher();

const canvasContextReset = initCanvasContextObserver(
win,
blockClass,
blockSelector,
unblockSelector,
false,
);
const canvas2DReset = initCanvas2DMutationObserver(
this.processMutation.bind(this),
win,
blockClass,
blockSelector,
unblockSelector,
);

const canvasWebGL1and2Reset = initCanvasWebGLMutationObserver(
this.processMutation.bind(this),
win,
blockClass,
blockSelector,
unblockSelector,
this.mirror,
);

this.resetObservers = () => {
canvasContextReset();
canvas2DReset();
canvasWebGL1and2Reset();
};
return rafId;
}

private startPendingCanvasMutationFlusher() {
Expand Down

0 comments on commit 6d12350

Please sign in to comment.