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: { sampling: {
canvas: 15, canvas: 15,
}, },
// optional image format settings
dataURLOptions: {
type: 'image/webp',
quality: 0.6,
},
}); });
``` ```

View File

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

View File

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

View File

@@ -169,6 +169,7 @@ export default class MutationBuffer {
private recordCanvas: observerParam['recordCanvas']; private recordCanvas: observerParam['recordCanvas'];
private inlineImages: observerParam['inlineImages']; private inlineImages: observerParam['inlineImages'];
private slimDOMOptions: observerParam['slimDOMOptions']; private slimDOMOptions: observerParam['slimDOMOptions'];
private dataURLOptions: observerParam['dataURLOptions'];
private doc: observerParam['doc']; private doc: observerParam['doc'];
private mirror: observerParam['mirror']; private mirror: observerParam['mirror'];
private iframeManager: observerParam['iframeManager']; private iframeManager: observerParam['iframeManager'];
@@ -191,6 +192,7 @@ export default class MutationBuffer {
'recordCanvas', 'recordCanvas',
'inlineImages', 'inlineImages',
'slimDOMOptions', 'slimDOMOptions',
'dataURLOptions',
'doc', 'doc',
'mirror', 'mirror',
'iframeManager', 'iframeManager',
@@ -301,6 +303,7 @@ export default class MutationBuffer {
maskTextFn: this.maskTextFn, maskTextFn: this.maskTextFn,
maskInputFn: this.maskInputFn, maskInputFn: this.maskInputFn,
slimDOMOptions: this.slimDOMOptions, slimDOMOptions: this.slimDOMOptions,
dataURLOptions: this.dataURLOptions,
recordCanvas: this.recordCanvas, recordCanvas: this.recordCanvas,
inlineImages: this.inlineImages, inlineImages: this.inlineImages,
onSerialize: (currentN) => { 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 { import type {
blockClass, blockClass,
canvasManagerMutationCallback, canvasManagerMutationCallback,
@@ -63,6 +63,7 @@ export class CanvasManager {
blockSelector: string | null; blockSelector: string | null;
mirror: Mirror; mirror: Mirror;
sampling?: 'all' | number; sampling?: 'all' | number;
dataURLOptions: DataURLOptions;
}) { }) {
const { const {
sampling = 'all', sampling = 'all',
@@ -70,6 +71,7 @@ export class CanvasManager {
blockClass, blockClass,
blockSelector, blockSelector,
recordCanvas, recordCanvas,
dataURLOptions,
} = options; } = options;
this.mutationCb = options.mutationCb; this.mutationCb = options.mutationCb;
this.mirror = options.mirror; this.mirror = options.mirror;
@@ -77,7 +79,9 @@ export class CanvasManager {
if (recordCanvas && sampling === 'all') if (recordCanvas && sampling === 'all')
this.initCanvasMutationObserver(win, blockClass, blockSelector); this.initCanvasMutationObserver(win, blockClass, blockSelector);
if (recordCanvas && typeof sampling === 'number') if (recordCanvas && typeof sampling === 'number')
this.initCanvasFPSObserver(sampling, win, blockClass, blockSelector); this.initCanvasFPSObserver(sampling, win, blockClass, blockSelector, {
dataURLOptions,
});
} }
private processMutation: canvasManagerMutationCallback = ( private processMutation: canvasManagerMutationCallback = (
@@ -102,6 +106,9 @@ export class CanvasManager {
win: IWindow, win: IWindow,
blockClass: blockClass, blockClass: blockClass,
blockSelector: string | null, blockSelector: string | null,
options: {
dataURLOptions: DataURLOptions;
},
) { ) {
const canvasContextReset = initCanvasContextObserver( const canvasContextReset = initCanvasContextObserver(
win, win,
@@ -202,6 +209,7 @@ export class CanvasManager {
bitmap, bitmap,
width: canvas.width, width: canvas.width,
height: canvas.height, height: canvas.height,
dataURLOptions: options.dataURLOptions,
}, },
[bitmap], [bitmap],
); );

View File

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

View File

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