Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions packages/rrweb-snapshot/src/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -675,7 +675,7 @@ function serializeElementNode(
} = options;
let needBlock = _isBlockedElement(n, blockClass, blockSelector);
const needMask = _isBlockedElement(n, maskTextClass, null);
const tagName = getValidTagName(n);
let tagName = getValidTagName(n);
let attributes: attributes = {};
const len = n.attributes.length;
for (let i = 0; i < len; i++) {
Expand Down Expand Up @@ -866,6 +866,19 @@ function serializeElementNode(
delete attributes.src; // prevent auto loading
}

if (inlineImages && tagName === 'video') {
const video = n as HTMLVideoElement;
if (video.src === '' || video.src.indexOf('blob:') !== -1) {
const { width, height } = n.getBoundingClientRect();
attributes = {
rr_width: `${width}px`,
rr_height: `${height}px`,
rr_inlined_video: true,
};
tagName = 'canvas';
}
}

return {
type: NodeType.Element,
tagName,
Expand Down Expand Up @@ -1361,7 +1374,7 @@ function snapshot(
}
: maskAllInputs;
const slimDOMOptions: SlimDOMOptions =
slimDOM === true || (slimDOM as unknown) === 'all'
slimDOM || (slimDOM as unknown) === 'all'
? // if true: set of sensible options that should not throw away any information
{
script: true,
Expand All @@ -1375,7 +1388,7 @@ function snapshot(
headMetaAuthorship: true,
headMetaVerification: true,
}
: slimDOM === false
: !slimDOM
? {}
: slimDOM;
return serializeNodeWithId(n, {
Expand Down
7 changes: 5 additions & 2 deletions packages/rrweb/src/record/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ function record<T = eventWithTime>(
hooks,
packFn,
sampling = {},
dataURLOptions = {},
mousemoveWait,
recordCanvas = false,
recordCrossOriginIframes = false,
Expand All @@ -92,6 +91,10 @@ function record<T = eventWithTime>(
errorHandler,
logger,
} = options;
const dataURLOptions = {
...options.dataURLOptions,
...options.sampling?.canvas?.dataURLOptions,
};

registerErrorHandler(errorHandler);

Expand Down Expand Up @@ -310,14 +313,14 @@ function record<T = eventWithTime>(

canvasManager = new CanvasManager({
recordCanvas,
recordVideos: inlineImages,
mutationCb: wrappedCanvasMutationEmit,
win: window,
blockClass,
blockSelector,
mirror,
sampling: sampling?.canvas?.fps,
dataURLOptions,
resizeQuality: sampling?.canvas?.resizeQuality,
resizeFactor: sampling?.canvas?.resizeFactor,
maxSnapshotDimension: sampling?.canvas?.maxSnapshotDimension,
logger: logger,
Expand Down
139 changes: 107 additions & 32 deletions packages/rrweb/src/record/observers/canvas/canvas-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,14 @@ export class CanvasManager {

constructor(options: {
recordCanvas: boolean;
recordVideos: boolean;
mutationCb: canvasMutationCallback;
win: IWindow;
blockClass: blockClass;
blockSelector: string | null;
mirror: Mirror;
sampling?: 'all' | number;
dataURLOptions: DataURLOptions;
resizeQuality?: 'pixelated' | 'low' | 'medium' | 'high';
resizeFactor?: number;
maxSnapshotDimension?: number;
logger?: {
Expand All @@ -86,6 +86,7 @@ export class CanvasManager {
blockClass,
blockSelector,
recordCanvas,
recordVideos,
dataURLOptions,
} = options;
this.mutationCb = options.mutationCb;
Expand All @@ -96,29 +97,29 @@ export class CanvasManager {
this.initCanvasMutationObserver(win, blockClass, blockSelector);
if (recordCanvas && typeof sampling === 'number')
this.initCanvasFPSObserver(
recordVideos,
sampling,
win,
blockClass,
blockSelector,
{
dataURLOptions,
},
options.resizeQuality,
options.resizeFactor,
options.maxSnapshotDimension,
);
}

private debug(
canvas?: HTMLCanvasElement,
element: HTMLCanvasElement | HTMLVideoElement,
...args: Parameters<typeof console.log>
) {
if (!this.logger) return;
let prefix = '[highlight-canvas]';
if (canvas) {
prefix += ` [ctx:${(canvas as ICanvas).__context}]`;
let prefix = `[highlight-${element.tagName.toLowerCase()}]`;
if (element.tagName === 'canvas') {
prefix += ` [ctx:${(element as ICanvas).__context}]`;
}
this.logger.debug(prefix, canvas, ...args);
this.logger.debug(prefix, element, ...args);
}

private processMutation: canvasManagerMutationCallback = (
Expand All @@ -139,14 +140,14 @@ export class CanvasManager {
};

private initCanvasFPSObserver(
recordVideos: boolean,
fps: number,
win: IWindow,
blockClass: blockClass,
blockSelector: string | null,
options: {
dataURLOptions: DataURLOptions;
},
resizeQuality?: 'pixelated' | 'low' | 'medium' | 'high',
resizeFactor?: number,
maxSnapshotDimension?: number,
) {
Expand All @@ -164,14 +165,14 @@ export class CanvasManager {

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

const { base64, type, canvasWidth, canvasHeight } = e.data;
const { base64, type, dx, dy, dw, dh } = e.data;
this.mutationCb({
id,
type: CanvasContext['2D'],
commands: [
{
property: 'clearRect', // wipe canvas
args: [0, 0, canvasWidth, canvasHeight],
args: [dx, dy, dw, dh],
},
{
property: 'drawImage', // draws (semi-transparent) image
Expand All @@ -186,10 +187,10 @@ export class CanvasManager {
},
],
} as CanvasArg,
0,
0,
canvasWidth,
canvasHeight,
dx,
dy,
dw,
dh,
],
},
],
Expand All @@ -211,12 +212,25 @@ export class CanvasManager {
return matchedCanvas;
};

const takeCanvasSnapshots = (timestamp: DOMHighResTimeStamp) => {
const getVideos = (): HTMLVideoElement[] => {
const matchedVideos: HTMLVideoElement[] = [];
if (recordVideos) {
win.document.querySelectorAll('video').forEach((video) => {
if (video.src !== '' && video.src.indexOf('blob:') === -1) return;
if (!isBlocked(video, blockClass, blockSelector, true)) {
matchedVideos.push(video);
}
});
}
return matchedVideos;
};

const takeSnapshots = (timestamp: DOMHighResTimeStamp) => {
if (
lastSnapshotTime &&
timestamp - lastSnapshotTime < timeBetweenSnapshots
) {
rafId = requestAnimationFrame(takeCanvasSnapshots);
rafId = requestAnimationFrame(takeSnapshots);
return;
}
lastSnapshotTime = timestamp;
Expand Down Expand Up @@ -266,43 +280,104 @@ export class CanvasManager {
const width = canvas.width * scale;
const height = canvas.height * scale;

window.performance.mark(`canvas-${canvas.id}-snapshot`);
const bitmap = await createImageBitmap(canvas, {
resizeQuality: resizeQuality || 'low',
resizeWidth: width,
resizeHeight: height,
});
this.debug(
canvas,
'took a snapshot in',
window.performance.measure(`canvas-snapshot`),
);
window.performance.mark(`canvas-postMessage`);
this.debug(canvas, 'created image bitmap');
worker.postMessage(
{
id,
bitmap,
width,
height,
canvasWidth: canvas.width,
canvasHeight: canvas.height,
dx: 0,
dy: 0,
dw: canvas.width,
dh: canvas.height,
dataURLOptions: options.dataURLOptions,
},
[bitmap],
);
this.debug(
canvas,
'send message in',
window.performance.measure(`canvas-postMessage`),
this.debug(canvas, 'sent message');
} finally {
snapshotInProgressMap.set(id, false);
}
});
getVideos().forEach(async (video: HTMLVideoElement) => {
this.debug(video, 'starting video snapshotting');
const id = this.mirror.getId(video);
if (snapshotInProgressMap.get(id)) {
this.debug(video, 'video snapshotting already in progress for', id);
return;
}
snapshotInProgressMap.set(id, true);
try {
const { width: boxWidth, height: boxHeight } =
video.getBoundingClientRect();
const { actualWidth, actualHeight } = {
actualWidth: video.videoWidth,
actualHeight: video.videoHeight,
};
const maxDim = Math.max(actualWidth, actualHeight);
let scale = resizeFactor || 1;
if (maxSnapshotDimension) {
scale = Math.min(scale, maxSnapshotDimension / maxDim);
}
const width = actualWidth * scale;
const height = actualHeight * scale;

const bitmap = await createImageBitmap(video, {
resizeWidth: width,
resizeHeight: height,
});

let outputScale = Math.max(boxWidth, boxHeight) / maxDim;
const outputWidth = actualWidth * outputScale;
const outputHeight = actualHeight * outputScale;
const offsetX = (boxWidth - outputWidth) / 2;
const offsetY = (boxHeight - outputHeight) / 2;
this.debug(video, 'created image bitmap', {
actualWidth,
actualHeight,
boxWidth,
boxHeight,
outputWidth,
outputHeight,
resizeWidth: width,
resizeHeight: height,
scale,
outputScale,
offsetX,
offsetY,
});

worker.postMessage(
{
id,
bitmap,
width,
height,
dx: offsetX,
dy: offsetY,
dw: outputWidth,
dh: outputHeight,
dataURLOptions: options.dataURLOptions,
},
[bitmap],
);
this.debug(video, 'send message');
} catch (e) {
this.debug(video, 'failed to snapshot', e);
} finally {
snapshotInProgressMap.set(id, false);
}
});
rafId = requestAnimationFrame(takeCanvasSnapshots);

rafId = requestAnimationFrame(takeSnapshots);
};

rafId = requestAnimationFrame(takeCanvasSnapshots);
rafId = requestAnimationFrame(takeSnapshots);

this.resetObservers = () => {
canvasContextReset();
Expand Down
17 changes: 6 additions & 11 deletions packages/rrweb/src/record/workers/image-bitmap-data-url-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,8 @@ const worker: ImageBitmapDataURLResponseWorker = self;
// eslint-disable-next-line @typescript-eslint/no-misused-promises
worker.onmessage = async function (e) {
if ('OffscreenCanvas' in globalThis) {
const {
id,
bitmap,
width,
height,
canvasWidth,
canvasHeight,
dataURLOptions,
} = e.data;
const { id, bitmap, width, height, dx, dy, dw, dh, dataURLOptions } =
e.data;

const transparentBase64 = getTransparentBlobFor(
width,
Expand Down Expand Up @@ -89,8 +82,10 @@ worker.onmessage = async function (e) {
base64,
width,
height,
canvasWidth,
canvasHeight,
dx,
dy,
dw,
dh,
});
lastBlobMap.set(id, base64);
} else {
Expand Down
6 changes: 3 additions & 3 deletions packages/types/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"dev": "vite",
"check-types": "tsc -noEmit",
"lint": "yarn eslint src/**/*.ts",
"typegen": "vite build"
"typegen": "tsc --emitDeclarationOnly -d",
"build": "vite build"
},
"homepage": "https://github.com/rrweb-io/rrweb/tree/main/packages/@rrweb/types#readme",
"bugs": {
Expand Down Expand Up @@ -40,8 +41,7 @@
],
"devDependencies": {
"typescript": "^4.7.3",
"vite": "^3.2.0-beta.2",
"vite-plugin-dts": "^1.6.6"
"vite": "^3.2.6"
},
"dependencies": {
"@highlight-run/rrweb-snapshot": "workspace:*"
Expand Down
Loading