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