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:
@@ -21,6 +21,11 @@ rrweb.record({
|
||||
sampling: {
|
||||
canvas: 15,
|
||||
},
|
||||
// optional image format settings
|
||||
dataURLOptions: {
|
||||
type: 'image/webp',
|
||||
quality: 0.6,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -21,6 +21,11 @@ rrweb.record({
|
||||
sampling: {
|
||||
canvas: 15,
|
||||
},
|
||||
// 图像的格式
|
||||
dataURLOptions: {
|
||||
type: 'image/webp',
|
||||
quality: 0.6,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
1
guide.md
1
guide.md
@@ -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) |
|
||||
|
||||
@@ -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 | 是否记录页面中的字体文件 |
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user