feat: add dataURLOptions parameter control canvas image format and quality (#967)

* feat: record add dataURLOptions parameter control canvas image format and quality

* 解决 build failed

* Update docs/recipes/canvas.md

* Apply formatting changes

* Update canvas-manager.ts

Fix the error caused when I resolved the merge conflict

Co-authored-by: Justin Halsall <Juice10@users.noreply.github.com>
Co-authored-by: Yun Feng <yun.feng@anu.edu.au>
Co-authored-by: Mark-Fenng <Mark-Fenng@users.noreply.github.com>
Co-authored-by: Yun Feng <yun.feng0817@gmail.com>
This commit is contained in:
何遇
2026-04-01 12:00:00 +08:00
committed by GitHub
parent 49349b12e1
commit 268229b8b3
9 changed files with 45 additions and 6 deletions

View File

@@ -21,6 +21,11 @@ rrweb.record({
sampling: {
canvas: 15,
},
// optional image format settings
dataURLOptions: {
type: 'image/webp',
quality: 0.6,
},
});
```

View File

@@ -21,6 +21,11 @@ rrweb.record({
sampling: {
canvas: 15,
},
// 图像的格式
dataURLOptions: {
type: 'image/webp',
quality: 0.6,
},
});
```

View File

@@ -151,6 +151,7 @@ The parameter of `rrweb.record` accepts the following options.
| maskInputFn | - | customize mask input content recording logic |
| maskTextFn | - | customize mask text content recording logic |
| slimDOMOptions | {} | remove unnecessary parts of the DOM <br />refer to the [list](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L97-L108) |
| dataURLOptions | {} | Canvas image format and quality ,This parameter will be passed to the OffscreenCanvas.convertToBlob(),Using this parameter effectively reduces the size of the recorded data |
| inlineStylesheet | true | whether to inline the stylesheet in the events |
| hooks | {} | hooks for events<br />refer to the [list](https://github.com/rrweb-io/rrweb/blob/9488deb6d54a5f04350c063d942da5e96ab74075/src/types.ts#L207) |
| packFn | - | refer to the [storage optimization recipe](./docs/recipes/optimize-storage.md) |

View File

@@ -151,6 +151,7 @@ setInterval(save, 10 * 1000);
| hooks | {} | 各类事件的回调<br />类型详见[列表](https://github.com/rrweb-io/rrweb/blob/9488deb6d54a5f04350c063d942da5e96ab74075/src/types.ts#L207) |
| packFn | - | 数据压缩函数,详见[优化存储策略](./docs/recipes/optimize-storage.zh_CN.md) |
| sampling | - | 数据抽样策略,详见[优化存储策略](./docs/recipes/optimize-storage.zh_CN.md) |
| dataURLOptions | {} | Canvas 图像快照的格式和质量,这个参数将传递给 OffscreenCanvas.convertToBlob(),使用这个参数能有效减小录制数据的大小 |
| recordCanvas | false | 是否记录 canvas 内容, 可用选项false, true |
| inlineImages | false | 是否将图片内容记内联录制 |
| collectFonts | false | 是否记录页面中的字体文件 |

View File

@@ -64,6 +64,7 @@ function record<T = eventWithTime>(
hooks,
packFn,
sampling = {},
dataURLOptions = {},
mousemoveWait,
recordCanvas = false,
userTriggeredOnInput = false,
@@ -240,6 +241,7 @@ function record<T = eventWithTime>(
blockSelector,
mirror,
sampling: sampling.canvas,
dataURLOptions,
});
const shadowDomManager = new ShadowDomManager({
@@ -252,6 +254,7 @@ function record<T = eventWithTime>(
maskTextSelector,
inlineStylesheet,
maskInputOptions,
dataURLOptions,
maskTextFn,
maskInputFn,
recordCanvas,
@@ -290,6 +293,7 @@ function record<T = eventWithTime>(
maskAllInputs: maskInputOptions,
maskTextFn,
slimDOM: slimDOMOptions,
dataURLOptions,
recordCanvas,
inlineImages,
onSerialize: (n) => {
@@ -471,6 +475,7 @@ function record<T = eventWithTime>(
keepIframeSrcFn,
blockSelector,
slimDOMOptions,
dataURLOptions,
mirror,
iframeManager,
stylesheetManager,

View File

@@ -169,6 +169,7 @@ export default class MutationBuffer {
private recordCanvas: observerParam['recordCanvas'];
private inlineImages: observerParam['inlineImages'];
private slimDOMOptions: observerParam['slimDOMOptions'];
private dataURLOptions: observerParam['dataURLOptions'];
private doc: observerParam['doc'];
private mirror: observerParam['mirror'];
private iframeManager: observerParam['iframeManager'];
@@ -191,6 +192,7 @@ export default class MutationBuffer {
'recordCanvas',
'inlineImages',
'slimDOMOptions',
'dataURLOptions',
'doc',
'mirror',
'iframeManager',
@@ -301,6 +303,7 @@ export default class MutationBuffer {
maskTextFn: this.maskTextFn,
maskInputFn: this.maskInputFn,
slimDOMOptions: this.slimDOMOptions,
dataURLOptions: this.dataURLOptions,
recordCanvas: this.recordCanvas,
inlineImages: this.inlineImages,
onSerialize: (currentN) => {

View File

@@ -1,4 +1,4 @@
import type { ICanvas, Mirror } from 'rrweb-snapshot';
import type { ICanvas, Mirror, DataURLOptions } from 'rrweb-snapshot';
import type {
blockClass,
canvasManagerMutationCallback,
@@ -63,6 +63,7 @@ export class CanvasManager {
blockSelector: string | null;
mirror: Mirror;
sampling?: 'all' | number;
dataURLOptions: DataURLOptions;
}) {
const {
sampling = 'all',
@@ -70,6 +71,7 @@ export class CanvasManager {
blockClass,
blockSelector,
recordCanvas,
dataURLOptions,
} = options;
this.mutationCb = options.mutationCb;
this.mirror = options.mirror;
@@ -77,7 +79,9 @@ export class CanvasManager {
if (recordCanvas && sampling === 'all')
this.initCanvasMutationObserver(win, blockClass, blockSelector);
if (recordCanvas && typeof sampling === 'number')
this.initCanvasFPSObserver(sampling, win, blockClass, blockSelector);
this.initCanvasFPSObserver(sampling, win, blockClass, blockSelector, {
dataURLOptions,
});
}
private processMutation: canvasManagerMutationCallback = (
@@ -102,6 +106,9 @@ export class CanvasManager {
win: IWindow,
blockClass: blockClass,
blockSelector: string | null,
options: {
dataURLOptions: DataURLOptions;
},
) {
const canvasContextReset = initCanvasContextObserver(
win,
@@ -202,6 +209,7 @@ export class CanvasManager {
bitmap,
width: canvas.width,
height: canvas.height,
dataURLOptions: options.dataURLOptions,
},
[bitmap],
);

View File

@@ -1,4 +1,5 @@
import { encode } from 'base64-arraybuffer';
import type { DataURLOptions } from 'rrweb-snapshot';
import type {
ImageBitmapDataURLWorkerParams,
ImageBitmapDataURLWorkerResponse,
@@ -25,12 +26,13 @@ interface ImageBitmapDataURLResponseWorker {
async function getTransparentBlobFor(
width: number,
height: number,
dataURLOptions: DataURLOptions,
): Promise<string> {
const id = `${width}-${height}`;
if (transparentBlobMap.has(id)) return transparentBlobMap.get(id)!;
const offscreen = new OffscreenCanvas(width, height);
offscreen.getContext('2d'); // creates rendering context for `converToBlob`
const blob = await offscreen.convertToBlob(); // takes a while
const blob = await offscreen.convertToBlob(dataURLOptions); // takes a while
const arrayBuffer = await blob.arrayBuffer();
const base64 = encode(arrayBuffer); // cpu intensive
transparentBlobMap.set(id, base64);
@@ -45,16 +47,20 @@ 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, dataURLOptions } = e.data;
const transparentBase64 = getTransparentBlobFor(width, height);
const transparentBase64 = getTransparentBlobFor(
width,
height,
dataURLOptions,
);
const offscreen = new OffscreenCanvas(width, height);
const ctx = offscreen.getContext('2d')!;
ctx.drawImage(bitmap, 0, 0);
bitmap.close();
const blob = await offscreen.convertToBlob(); // takes a while
const blob = await offscreen.convertToBlob(dataURLOptions); // takes a while
const type = blob.type;
const arrayBuffer = await blob.arrayBuffer();
const base64 = encode(arrayBuffer); // cpu intensive

View File

@@ -6,6 +6,7 @@ import type {
SlimDOMOptions,
MaskInputFn,
MaskTextFn,
DataURLOptions,
} from 'rrweb-snapshot';
import type { PackFn, UnpackFn } from './packer/base';
import type { IframeManager } from './record/iframe-manager';
@@ -251,6 +252,7 @@ export type recordOptions<T> = {
hooks?: hooksParam;
packFn?: PackFn;
sampling?: SamplingStrategy;
dataURLOptions?: DataURLOptions;
recordCanvas?: boolean;
userTriggeredOnInput?: boolean;
collectFonts?: boolean;
@@ -290,6 +292,7 @@ export type observerParam = {
userTriggeredOnInput: boolean;
collectFonts: boolean;
slimDOMOptions: SlimDOMOptions;
dataURLOptions: DataURLOptions;
doc: Document;
mirror: Mirror;
iframeManager: IframeManager;
@@ -323,6 +326,7 @@ export type MutationBufferParam = Pick<
| 'recordCanvas'
| 'inlineImages'
| 'slimDOMOptions'
| 'dataURLOptions'
| 'doc'
| 'mirror'
| 'iframeManager'
@@ -563,6 +567,7 @@ export type ImageBitmapDataURLWorkerParams = {
bitmap: ImageBitmap;
width: number;
height: number;
dataURLOptions: DataURLOptions;
};
export type ImageBitmapDataURLWorkerResponse =