Skip to content

Commit

Permalink
support canvas downsampling configuration (#89)
Browse files Browse the repository at this point in the history
canvas recording takes snapshots at full canvas resolution
this is too slow to record at higher fps.
allow specifying canvas manager ratio and quality so that
the canvas can be imaged at lower quality and resolution to allow higher FPS recording

we now support the following `record` parameters for canvas settings: 
* sampling.canvas.fps
* sampling.canvas.resizeQuality
* sampling.canvas.resizeFactor
* sampling.canvas.maxSnapshotDimension

sample recording @ 0.2 factor, low quality, 15 fps
https://app.highlight.run/1/sessions/KnAGq8rRSnafG7zXwp8ahpzFAqFd
  • Loading branch information
Vadman97 authored Aug 10, 2022
1 parent 3ba5335 commit e891814
Show file tree
Hide file tree
Showing 6 changed files with 78 additions and 18 deletions.
2 changes: 1 addition & 1 deletion packages/rrweb-player/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"typescript": "^4.7.3"
},
"dependencies": {
"@highlight-run/rrweb": "2.1.7",
"@highlight-run/rrweb": "2.1.8",
"@tsconfig/svelte": "^1.0.1"
},
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/rrweb/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@highlight-run/rrweb",
"version": "2.1.7",
"version": "2.1.8",
"description": "record and replay the web",
"scripts": {
"prepare": "npm run prepack",
Expand Down
5 changes: 4 additions & 1 deletion packages/rrweb/src/record/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,10 @@ function record<T = eventWithTime>(
win: window,
blockClass,
mirror,
sampling: sampling.canvas,
sampling: sampling?.canvas?.fps,
resizeQuality: sampling?.canvas?.resizeQuality,
resizeFactor: sampling?.canvas?.resizeFactor,
maxSnapshotDimension: sampling?.canvas?.maxSnapshotDimension,
});

const shadowDomManager = new ShadowDomManager({
Expand Down
41 changes: 35 additions & 6 deletions packages/rrweb/src/record/observers/canvas/canvas-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ export class CanvasManager {
blockClass: blockClass;
mirror: Mirror;
sampling?: 'all' | number;
resizeQuality?: 'pixelated' | 'low' | 'medium' | 'high';
resizeFactor?: number;
maxSnapshotDimension?: number;
}) {
const { sampling = 'all', win, blockClass, recordCanvas } = options;
this.mutationCb = options.mutationCb;
Expand All @@ -69,7 +72,14 @@ export class CanvasManager {
if (recordCanvas && sampling === 'all')
this.initCanvasMutationObserver(win, blockClass);
if (recordCanvas && typeof sampling === 'number')
this.initCanvasFPSObserver(sampling, win, blockClass);
this.initCanvasFPSObserver(
sampling,
win,
blockClass,
options.resizeQuality,
options.resizeFactor,
options.maxSnapshotDimension,
);
}

private processMutation: canvasManagerMutationCallback = (
Expand All @@ -93,6 +103,9 @@ export class CanvasManager {
fps: number,
win: IWindow,
blockClass: blockClass,
resizeQuality?: 'pixelated' | 'low' | 'medium' | 'high',
resizeFactor?: number,
maxSnapshotDimension?: number,
) {
const canvasContextReset = initCanvasContextObserver(win, blockClass);
const snapshotInProgressMap: Map<number, boolean> = new Map();
Expand All @@ -103,14 +116,14 @@ export class CanvasManager {

if (!('base64' in e.data)) return;

const { base64, type, width, height } = e.data;
const { base64, type, canvasWidth, canvasHeight } = e.data;
this.mutationCb({
id,
type: CanvasContext['2D'],
commands: [
{
property: 'clearRect', // wipe canvas
args: [0, 0, width, height],
args: [0, 0, canvasWidth, canvasHeight],
},
{
property: 'drawImage', // draws (semi-transparent) image
Expand All @@ -127,6 +140,8 @@ export class CanvasManager {
} as CanvasArg,
0,
0,
canvasWidth,
canvasHeight,
],
},
],
Expand Down Expand Up @@ -178,13 +193,27 @@ export class CanvasManager {
if (canvas.width === 0 || canvas.height === 0) {
return;
}
const bitmap = await createImageBitmap(canvas);
let scale = resizeFactor || 1;
if (maxSnapshotDimension) {
const maxDim = Math.max(canvas.width, canvas.height);
scale = Math.min(scale, maxSnapshotDimension / maxDim);
}
const width = canvas.width * scale;
const height = canvas.height * scale;

const bitmap = await createImageBitmap(canvas, {
resizeQuality: resizeQuality || 'low',
resizeWidth: width,
resizeHeight: height,
});
worker.postMessage(
{
id,
bitmap,
width: canvas.width,
height: canvas.height,
width,
height,
canvasWidth: canvas.width,
canvasHeight: canvas.height,
},
[bitmap],
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,14 @@ worker.onmessage = async function (e) {
if (!('OffscreenCanvas' in globalThis))
return worker.postMessage({ id: e.data.id });

const { id, bitmap, width, height } = e.data;
const { id, bitmap, width, height, canvasWidth, canvasHeight } = e.data;

const transparentBase64 = getTransparentBlobFor(width, height);

const offscreen = new OffscreenCanvas(width, height);
const ctx = offscreen.getContext('2d')!;

ctx.drawImage(bitmap, 0, 0);
ctx.drawImage(bitmap, 0, 0, width, height);
bitmap.close();
const blob = await offscreen.convertToBlob(); // takes a while
const type = blob.type;
Expand All @@ -73,6 +73,8 @@ worker.onmessage = async function (e) {
base64,
width,
height,
canvasWidth,
canvasHeight,
});
lastBlobMap.set(id, base64);
};
40 changes: 33 additions & 7 deletions packages/rrweb/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,32 @@ export type blockClass = string | RegExp;

export type maskTextClass = string | RegExp;

export type CanvasSamplingStrategy = Partial<{
/**
* 'all' will record every single canvas call
* number between 1 and 60, will record an image snapshots in a web-worker a (maximum) number of times per second.
* Number only supported where [`OffscreenCanvas`](http://mdn.io/offscreencanvas) is supported.
*/
fps: 'all' | number;
/**
* A scaling to apply to canvas shapshotting. Adjusts the resolution at which
* canvases are recorded by this multiple.
*/
resizeFactor: number;
/**
* The quality of canvas snapshots
*/
resizeQuality: 'pixelated' | 'low' | 'medium' | 'high';
/**
* 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;
}>;

export type SamplingStrategy = Partial<{
/**
* false means not to record mouse/touch move events
Expand Down Expand Up @@ -213,12 +239,8 @@ export type SamplingStrategy = Partial<{
* 'last' will only record the last input value while input a sequence of chars
*/
input: 'all' | 'last';
/**
* 'all' will record every single canvas call
* number between 1 and 60, will record an image snapshots in a web-worker a (maximum) number of times per second.
* Number only supported where [`OffscreenCanvas`](http://mdn.io/offscreencanvas) is supported.
*/
canvas: 'all' | number;

canvas: CanvasSamplingStrategy;
}>;

export type RecordPlugin<TOptions = unknown> = {
Expand Down Expand Up @@ -299,7 +321,7 @@ export type observerParam = {
stylesheetManager: StylesheetManager;
shadowDomManager: ShadowDomManager;
canvasManager: CanvasManager;
enableStrictPrivacy: boolean,
enableStrictPrivacy: boolean;
plugins: Array<{
observer: (
cb: (...arg: Array<unknown>) => void,
Expand Down Expand Up @@ -566,6 +588,8 @@ export type ImageBitmapDataURLWorkerParams = {
bitmap: ImageBitmap;
width: number;
height: number;
canvasWidth: number;
canvasHeight: number;
};

export type ImageBitmapDataURLWorkerResponse =
Expand All @@ -578,6 +602,8 @@ export type ImageBitmapDataURLWorkerResponse =
base64: string;
width: number;
height: number;
canvasWidth: number;
canvasHeight: number;
};

export type fontParam = {
Expand Down

0 comments on commit e891814

Please sign in to comment.