Skip to content

Commit

Permalink
keep 2d canvas serialization at lower resolution
Browse files Browse the repository at this point in the history
  • Loading branch information
Vadman97 committed Sep 23, 2024
1 parent 51c6e3d commit 06b2fb7
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 42 deletions.
59 changes: 27 additions & 32 deletions packages/rrweb-snapshot/src/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
type serializedElementNodeWithId,
type mediaAttributes,
type PrivacySettingOption,
type CanvasSamplingStrategy,
} from './types';
import {
Mirror,
Expand All @@ -32,6 +33,7 @@ import {
absolutifyURLs,
markCssSplits,
isElementSrcBlocked,
snapshot2DCanvas,
} from './utils';
import dom from '@rrweb/utils';

Expand Down Expand Up @@ -404,6 +406,7 @@ function serializeNode(
dataURLOptions?: DataURLOptions;
inlineImages: boolean;
recordCanvas: boolean;
canvasSampling: CanvasSamplingStrategy;
keepIframeSrcFn: KeepIframeSrcFn;
/**
* `newlyAddedElement: true` skips scrollTop and scrollLeft check
Expand All @@ -426,9 +429,9 @@ function serializeNode(
maskTextClass,
maskTextFn,
maskInputFn,
dataURLOptions = {},
inlineImages,
recordCanvas,
canvasSampling,
keepIframeSrcFn,
newlyAddedElement = false,
cssCaptured = false,
Expand Down Expand Up @@ -467,7 +470,7 @@ function serializeNode(
maskInputOptions,
maskInputFn,
maskTextClass,
dataURLOptions,
canvasSampling,
inlineImages,
recordCanvas,
keepIframeSrcFn,
Expand Down Expand Up @@ -589,9 +592,9 @@ function serializeElementNode(
maskInputOptions: MaskInputOptions;
maskInputFn: MaskInputFn | undefined;
maskTextClass: string | RegExp;
dataURLOptions?: DataURLOptions;
inlineImages: boolean;
recordCanvas: boolean;
canvasSampling: CanvasSamplingStrategy;
keepIframeSrcFn: KeepIframeSrcFn;
/**
* `newlyAddedElement: true` skips scrollTop and scrollLeft check
Expand All @@ -609,9 +612,9 @@ function serializeElementNode(
maskInputOptions = {},
maskInputFn,
maskTextClass,
dataURLOptions = {},
inlineImages,
recordCanvas,
canvasSampling,
keepIframeSrcFn,
newlyAddedElement = false,
privacySetting,
Expand Down Expand Up @@ -709,29 +712,23 @@ function serializeElementNode(
if ((n as ICanvas).__context === '2d') {
// only record this on 2d canvas
if (!is2DCanvasBlank(n as HTMLCanvasElement)) {
/** Start of Highlight Code
* Avoid capturing non-blank 2d canvas here - defer to canvas-manager.ts for better performance
attributes.rr_dataURL = (n as HTMLCanvasElement).toDataURL(
dataURLOptions.type,
dataURLOptions.quality,
attributes.rr_dataURL = snapshot2DCanvas(
n as HTMLCanvasElement,
canvasSampling,
);
End of Highlight Code **/
}
} else if (!('__context' in n)) {
// context is unknown, better not call getContext to trigger it
const canvasDataURL = (n as HTMLCanvasElement).toDataURL(
dataURLOptions.type,
dataURLOptions.quality,
const canvasDataURL = snapshot2DCanvas(
n as HTMLCanvasElement,
canvasSampling,
);

// create blank canvas of same dimensions
const blankCanvas = doc.createElement('canvas');
blankCanvas.width = (n as HTMLCanvasElement).width;
blankCanvas.height = (n as HTMLCanvasElement).height;
const blankCanvasDataURL = blankCanvas.toDataURL(
dataURLOptions.type,
dataURLOptions.quality,
);
const blankCanvasDataURL = snapshot2DCanvas(blankCanvas, canvasSampling);

// no need to save dataURL if it's the same as blank canvas
if (canvasDataURL !== blankCanvasDataURL) {
Expand Down Expand Up @@ -761,9 +758,9 @@ function serializeElementNode(
canvasService!.width = image.naturalWidth;
canvasService!.height = image.naturalHeight;
canvasCtx!.drawImage(image, 0, 0);
attributes.rr_dataURL = canvasService!.toDataURL(
dataURLOptions.type,
dataURLOptions.quality,
attributes.rr_dataURL = snapshot2DCanvas(
canvasService!,
canvasSampling,
);
} catch (err) {
if (image.crossOrigin !== 'anonymous') {
Expand Down Expand Up @@ -864,10 +861,7 @@ function serializeElementNode(
blankCanvas.width = (n as HTMLCanvasElement).width;
blankCanvas.height = (n as HTMLCanvasElement).height;

attributes.rr_dataURL = blankCanvas.toDataURL(
dataURLOptions.type,
dataURLOptions.quality,
);
attributes.rr_dataURL = snapshot2DCanvas(blankCanvas, canvasSampling);
}
}

Expand Down Expand Up @@ -1003,10 +997,10 @@ export function serializeNodeWithId(
maskTextFn: MaskTextFn | undefined;
maskInputFn: MaskInputFn | undefined;
slimDOMOptions: SlimDOMOptions;
dataURLOptions?: DataURLOptions;
keepIframeSrcFn?: KeepIframeSrcFn;
inlineImages?: boolean;
recordCanvas?: boolean;
canvasSampling?: CanvasSamplingStrategy;
preserveWhiteSpace?: boolean;
onSerialize?: (n: Node) => unknown;
onIframeLoad?: (
Expand Down Expand Up @@ -1036,9 +1030,9 @@ export function serializeNodeWithId(
maskTextFn,
maskInputFn,
slimDOMOptions,
dataURLOptions = {},
inlineImages = false,
recordCanvas = false,
canvasSampling = {},
onSerialize,
onIframeLoad,
iframeLoadTimeout = 5000,
Expand Down Expand Up @@ -1074,9 +1068,9 @@ export function serializeNodeWithId(
maskTextClass,
maskTextFn,
maskInputFn,
dataURLOptions,
inlineImages,
recordCanvas,
canvasSampling,
keepIframeSrcFn,
newlyAddedElement,
cssCaptured,
Expand Down Expand Up @@ -1169,9 +1163,9 @@ export function serializeNodeWithId(
maskTextFn,
maskInputFn,
slimDOMOptions,
dataURLOptions,
inlineImages,
recordCanvas,
canvasSampling,
preserveWhiteSpace,
onSerialize,
onIframeLoad,
Expand Down Expand Up @@ -1246,9 +1240,9 @@ export function serializeNodeWithId(
maskTextFn,
maskInputFn,
slimDOMOptions,
dataURLOptions,
inlineImages,
recordCanvas,
canvasSampling,
preserveWhiteSpace,
onSerialize,
onIframeLoad,
Expand Down Expand Up @@ -1299,9 +1293,9 @@ export function serializeNodeWithId(
maskTextFn,
maskInputFn,
slimDOMOptions,
dataURLOptions,
inlineImages,
recordCanvas,
canvasSampling,
preserveWhiteSpace,
onSerialize,
onIframeLoad,
Expand Down Expand Up @@ -1343,6 +1337,7 @@ function snapshot(
dataURLOptions?: DataURLOptions;
inlineImages?: boolean;
recordCanvas?: boolean;
canvasSampling?: CanvasSamplingStrategy;
preserveWhiteSpace?: boolean;
onSerialize?: (n: Node) => unknown;
onIframeLoad?: (
Expand All @@ -1368,11 +1363,11 @@ function snapshot(
inlineStylesheet = true,
inlineImages = false,
recordCanvas = false,
canvasSampling = {},
maskAllInputs = false,
maskTextFn,
maskInputFn,
slimDOM = false,
dataURLOptions,
preserveWhiteSpace,
onSerialize,
onIframeLoad,
Expand Down Expand Up @@ -1438,9 +1433,9 @@ function snapshot(
maskTextFn,
maskInputFn,
slimDOMOptions,
dataURLOptions,
inlineImages,
recordCanvas,
canvasSampling,
preserveWhiteSpace,
onSerialize,
onIframeLoad,
Expand Down
20 changes: 20 additions & 0 deletions packages/rrweb-snapshot/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,3 +227,23 @@ export type BuildCache = {
};

export type PrivacySettingOption = 'strict' | 'default' | 'none';

export type CanvasSamplingStrategy = Partial<{
/**
* A scaling to apply to canvas shapshotting. Adjusts the resolution at which
* canvases are recorded by this multiple.
*/
resizeFactor: number;
/**
* The maximum dimension to take canvas snapshots at.
* This setting takes precedence over resizeFactor if the resulting image size
* from the resizeFactor calculation is larger than this value.
* Eg: set to 600 to ensure that the canvas is saved with images no larger than 600px
* in either dimension (while preserving the original canvas aspect ratio).
*/
maxSnapshotDimension: number;
/**
* Adjust the quality of the canvas blob serialization.
*/
dataURLOptions: DataURLOptions;
}>;
50 changes: 41 additions & 9 deletions packages/rrweb-snapshot/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
documentTypeNode,
textNode,
elementNode,
CanvasSamplingStrategy,
} from './types';
import dom from '@rrweb/utils';
import { NodeType } from './types';
Expand Down Expand Up @@ -247,14 +248,14 @@ export function createMirror(): Mirror {
/* Start of Highlight Code */
// overwritten from rrweb
export function maskInputValue({
element,
maskInputOptions,
tagName,
type,
value,
overwriteRecord,
maskInputFn,
}: {
element,
maskInputOptions,
tagName,
type,
value,
overwriteRecord,
maskInputFn,
}: {
element: HTMLElement;
maskInputOptions: MaskInputOptions;
tagName: string;
Expand All @@ -270,7 +271,7 @@ export function maskInputValue({
maskInputOptions,
tagName,
type,
overwriteRecord
overwriteRecord,
})
) {
if (maskInputFn) {
Expand Down Expand Up @@ -589,4 +590,35 @@ export const maskedInputType = ({
);
};

export const resizeCanvas = (
canvas: HTMLCanvasElement,
sampling: CanvasSamplingStrategy,
) => {
let scale = sampling.resizeFactor || 1;
if (sampling.maxSnapshotDimension) {
const maxDim = Math.max(canvas.width, canvas.height);
scale = Math.min(scale, sampling.maxSnapshotDimension / maxDim);
}
const width = canvas.width * scale;
const height = canvas.height * scale;
return { width, height };
};

export const snapshot2DCanvas = (
canvas: HTMLCanvasElement,
sampling: CanvasSamplingStrategy,
) => {
const { width, height } = resizeCanvas(canvas, sampling);
const resizedCanvas = document.createElement('canvas');
const resizedContext = resizedCanvas.getContext('2d');
if (resizedContext === null) return '';
resizedCanvas.width = width;
resizedCanvas.height = height;
resizedContext.drawImage(canvas, 0, 0, width, height);
return resizedCanvas.toDataURL(
sampling.dataURLOptions?.type ?? 'image/webp',
sampling.dataURLOptions?.quality ?? 0.9,
);
};

/* End of Highlight Code */
2 changes: 1 addition & 1 deletion packages/rrweb/src/record/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,8 +400,8 @@ function record<T = eventWithTime>(
maskTextFn,
maskInputFn,
slimDOM: slimDOMOptions,
dataURLOptions,
recordCanvas,
canvasSampling: sampling.canvas,
inlineImages,
privacySetting,
onSerialize: (n) => {
Expand Down

0 comments on commit 06b2fb7

Please sign in to comment.