Skip to content

Commit 9c35a5c

Browse files
authored
support snapshotting video elements (#103)
local webcam video replay https://www.loom.com/share/c0a473d717d94b4f88ce010a440fb474 webcam image getting serialized ![image](https://user-images.githubusercontent.com/1351531/236074015-43475a04-9bfb-4101-ad32-d3bfb0337723.png) --------- Co-authored-by: Vadman97 <[email protected]>
1 parent cb37613 commit 9c35a5c

File tree

8 files changed

+152
-62
lines changed

8 files changed

+152
-62
lines changed

packages/rrweb-snapshot/src/snapshot.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -675,7 +675,7 @@ function serializeElementNode(
675675
} = options;
676676
let needBlock = _isBlockedElement(n, blockClass, blockSelector);
677677
const needMask = _isBlockedElement(n, maskTextClass, null);
678-
const tagName = getValidTagName(n);
678+
let tagName = getValidTagName(n);
679679
let attributes: attributes = {};
680680
const len = n.attributes.length;
681681
for (let i = 0; i < len; i++) {
@@ -866,6 +866,19 @@ function serializeElementNode(
866866
delete attributes.src; // prevent auto loading
867867
}
868868

869+
if (inlineImages && tagName === 'video') {
870+
const video = n as HTMLVideoElement;
871+
if (video.src === '' || video.src.indexOf('blob:') !== -1) {
872+
const { width, height } = n.getBoundingClientRect();
873+
attributes = {
874+
rr_width: `${width}px`,
875+
rr_height: `${height}px`,
876+
rr_inlined_video: true,
877+
};
878+
tagName = 'canvas';
879+
}
880+
}
881+
869882
return {
870883
type: NodeType.Element,
871884
tagName,
@@ -1361,7 +1374,7 @@ function snapshot(
13611374
}
13621375
: maskAllInputs;
13631376
const slimDOMOptions: SlimDOMOptions =
1364-
slimDOM === true || (slimDOM as unknown) === 'all'
1377+
slimDOM || (slimDOM as unknown) === 'all'
13651378
? // if true: set of sensible options that should not throw away any information
13661379
{
13671380
script: true,
@@ -1375,7 +1388,7 @@ function snapshot(
13751388
headMetaAuthorship: true,
13761389
headMetaVerification: true,
13771390
}
1378-
: slimDOM === false
1391+
: !slimDOM
13791392
? {}
13801393
: slimDOM;
13811394
return serializeNodeWithId(n, {

packages/rrweb/src/record/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ function record<T = eventWithTime>(
7575
hooks,
7676
packFn,
7777
sampling = {},
78-
dataURLOptions = {},
7978
mousemoveWait,
8079
recordCanvas = false,
8180
recordCrossOriginIframes = false,
@@ -92,6 +91,10 @@ function record<T = eventWithTime>(
9291
errorHandler,
9392
logger,
9493
} = options;
94+
const dataURLOptions = {
95+
...options.dataURLOptions,
96+
...options.sampling?.canvas?.dataURLOptions,
97+
};
9598

9699
registerErrorHandler(errorHandler);
97100

@@ -310,14 +313,14 @@ function record<T = eventWithTime>(
310313

311314
canvasManager = new CanvasManager({
312315
recordCanvas,
316+
recordVideos: inlineImages,
313317
mutationCb: wrappedCanvasMutationEmit,
314318
win: window,
315319
blockClass,
316320
blockSelector,
317321
mirror,
318322
sampling: sampling?.canvas?.fps,
319323
dataURLOptions,
320-
resizeQuality: sampling?.canvas?.resizeQuality,
321324
resizeFactor: sampling?.canvas?.resizeFactor,
322325
maxSnapshotDimension: sampling?.canvas?.maxSnapshotDimension,
323326
logger: logger,

packages/rrweb/src/record/observers/canvas/canvas-manager.ts

Lines changed: 107 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,14 @@ export class CanvasManager {
6565

6666
constructor(options: {
6767
recordCanvas: boolean;
68+
recordVideos: boolean;
6869
mutationCb: canvasMutationCallback;
6970
win: IWindow;
7071
blockClass: blockClass;
7172
blockSelector: string | null;
7273
mirror: Mirror;
7374
sampling?: 'all' | number;
7475
dataURLOptions: DataURLOptions;
75-
resizeQuality?: 'pixelated' | 'low' | 'medium' | 'high';
7676
resizeFactor?: number;
7777
maxSnapshotDimension?: number;
7878
logger?: {
@@ -86,6 +86,7 @@ export class CanvasManager {
8686
blockClass,
8787
blockSelector,
8888
recordCanvas,
89+
recordVideos,
8990
dataURLOptions,
9091
} = options;
9192
this.mutationCb = options.mutationCb;
@@ -96,29 +97,29 @@ export class CanvasManager {
9697
this.initCanvasMutationObserver(win, blockClass, blockSelector);
9798
if (recordCanvas && typeof sampling === 'number')
9899
this.initCanvasFPSObserver(
100+
recordVideos,
99101
sampling,
100102
win,
101103
blockClass,
102104
blockSelector,
103105
{
104106
dataURLOptions,
105107
},
106-
options.resizeQuality,
107108
options.resizeFactor,
108109
options.maxSnapshotDimension,
109110
);
110111
}
111112

112113
private debug(
113-
canvas?: HTMLCanvasElement,
114+
element: HTMLCanvasElement | HTMLVideoElement,
114115
...args: Parameters<typeof console.log>
115116
) {
116117
if (!this.logger) return;
117-
let prefix = '[highlight-canvas]';
118-
if (canvas) {
119-
prefix += ` [ctx:${(canvas as ICanvas).__context}]`;
118+
let prefix = `[highlight-${element.tagName.toLowerCase()}]`;
119+
if (element.tagName === 'canvas') {
120+
prefix += ` [ctx:${(element as ICanvas).__context}]`;
120121
}
121-
this.logger.debug(prefix, canvas, ...args);
122+
this.logger.debug(prefix, element, ...args);
122123
}
123124

124125
private processMutation: canvasManagerMutationCallback = (
@@ -139,14 +140,14 @@ export class CanvasManager {
139140
};
140141

141142
private initCanvasFPSObserver(
143+
recordVideos: boolean,
142144
fps: number,
143145
win: IWindow,
144146
blockClass: blockClass,
145147
blockSelector: string | null,
146148
options: {
147149
dataURLOptions: DataURLOptions;
148150
},
149-
resizeQuality?: 'pixelated' | 'low' | 'medium' | 'high',
150151
resizeFactor?: number,
151152
maxSnapshotDimension?: number,
152153
) {
@@ -164,14 +165,14 @@ export class CanvasManager {
164165

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

167-
const { base64, type, canvasWidth, canvasHeight } = e.data;
168+
const { base64, type, dx, dy, dw, dh } = e.data;
168169
this.mutationCb({
169170
id,
170171
type: CanvasContext['2D'],
171172
commands: [
172173
{
173174
property: 'clearRect', // wipe canvas
174-
args: [0, 0, canvasWidth, canvasHeight],
175+
args: [dx, dy, dw, dh],
175176
},
176177
{
177178
property: 'drawImage', // draws (semi-transparent) image
@@ -186,10 +187,10 @@ export class CanvasManager {
186187
},
187188
],
188189
} as CanvasArg,
189-
0,
190-
0,
191-
canvasWidth,
192-
canvasHeight,
190+
dx,
191+
dy,
192+
dw,
193+
dh,
193194
],
194195
},
195196
],
@@ -211,12 +212,25 @@ export class CanvasManager {
211212
return matchedCanvas;
212213
};
213214

214-
const takeCanvasSnapshots = (timestamp: DOMHighResTimeStamp) => {
215+
const getVideos = (): HTMLVideoElement[] => {
216+
const matchedVideos: HTMLVideoElement[] = [];
217+
if (recordVideos) {
218+
win.document.querySelectorAll('video').forEach((video) => {
219+
if (video.src !== '' && video.src.indexOf('blob:') === -1) return;
220+
if (!isBlocked(video, blockClass, blockSelector, true)) {
221+
matchedVideos.push(video);
222+
}
223+
});
224+
}
225+
return matchedVideos;
226+
};
227+
228+
const takeSnapshots = (timestamp: DOMHighResTimeStamp) => {
215229
if (
216230
lastSnapshotTime &&
217231
timestamp - lastSnapshotTime < timeBetweenSnapshots
218232
) {
219-
rafId = requestAnimationFrame(takeCanvasSnapshots);
233+
rafId = requestAnimationFrame(takeSnapshots);
220234
return;
221235
}
222236
lastSnapshotTime = timestamp;
@@ -266,43 +280,104 @@ export class CanvasManager {
266280
const width = canvas.width * scale;
267281
const height = canvas.height * scale;
268282

269-
window.performance.mark(`canvas-${canvas.id}-snapshot`);
270283
const bitmap = await createImageBitmap(canvas, {
271-
resizeQuality: resizeQuality || 'low',
272284
resizeWidth: width,
273285
resizeHeight: height,
274286
});
275-
this.debug(
276-
canvas,
277-
'took a snapshot in',
278-
window.performance.measure(`canvas-snapshot`),
279-
);
280-
window.performance.mark(`canvas-postMessage`);
287+
this.debug(canvas, 'created image bitmap');
281288
worker.postMessage(
282289
{
283290
id,
284291
bitmap,
285292
width,
286293
height,
287-
canvasWidth: canvas.width,
288-
canvasHeight: canvas.height,
294+
dx: 0,
295+
dy: 0,
296+
dw: canvas.width,
297+
dh: canvas.height,
289298
dataURLOptions: options.dataURLOptions,
290299
},
291300
[bitmap],
292301
);
293-
this.debug(
294-
canvas,
295-
'send message in',
296-
window.performance.measure(`canvas-postMessage`),
302+
this.debug(canvas, 'sent message');
303+
} finally {
304+
snapshotInProgressMap.set(id, false);
305+
}
306+
});
307+
getVideos().forEach(async (video: HTMLVideoElement) => {
308+
this.debug(video, 'starting video snapshotting');
309+
const id = this.mirror.getId(video);
310+
if (snapshotInProgressMap.get(id)) {
311+
this.debug(video, 'video snapshotting already in progress for', id);
312+
return;
313+
}
314+
snapshotInProgressMap.set(id, true);
315+
try {
316+
const { width: boxWidth, height: boxHeight } =
317+
video.getBoundingClientRect();
318+
const { actualWidth, actualHeight } = {
319+
actualWidth: video.videoWidth,
320+
actualHeight: video.videoHeight,
321+
};
322+
const maxDim = Math.max(actualWidth, actualHeight);
323+
let scale = resizeFactor || 1;
324+
if (maxSnapshotDimension) {
325+
scale = Math.min(scale, maxSnapshotDimension / maxDim);
326+
}
327+
const width = actualWidth * scale;
328+
const height = actualHeight * scale;
329+
330+
const bitmap = await createImageBitmap(video, {
331+
resizeWidth: width,
332+
resizeHeight: height,
333+
});
334+
335+
let outputScale = Math.max(boxWidth, boxHeight) / maxDim;
336+
const outputWidth = actualWidth * outputScale;
337+
const outputHeight = actualHeight * outputScale;
338+
const offsetX = (boxWidth - outputWidth) / 2;
339+
const offsetY = (boxHeight - outputHeight) / 2;
340+
this.debug(video, 'created image bitmap', {
341+
actualWidth,
342+
actualHeight,
343+
boxWidth,
344+
boxHeight,
345+
outputWidth,
346+
outputHeight,
347+
resizeWidth: width,
348+
resizeHeight: height,
349+
scale,
350+
outputScale,
351+
offsetX,
352+
offsetY,
353+
});
354+
355+
worker.postMessage(
356+
{
357+
id,
358+
bitmap,
359+
width,
360+
height,
361+
dx: offsetX,
362+
dy: offsetY,
363+
dw: outputWidth,
364+
dh: outputHeight,
365+
dataURLOptions: options.dataURLOptions,
366+
},
367+
[bitmap],
297368
);
369+
this.debug(video, 'send message');
370+
} catch (e) {
371+
this.debug(video, 'failed to snapshot', e);
298372
} finally {
299373
snapshotInProgressMap.set(id, false);
300374
}
301375
});
302-
rafId = requestAnimationFrame(takeCanvasSnapshots);
376+
377+
rafId = requestAnimationFrame(takeSnapshots);
303378
};
304379

305-
rafId = requestAnimationFrame(takeCanvasSnapshots);
380+
rafId = requestAnimationFrame(takeSnapshots);
306381

307382
this.resetObservers = () => {
308383
canvasContextReset();

packages/rrweb/src/record/workers/image-bitmap-data-url-worker.ts

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,8 @@ const worker: ImageBitmapDataURLResponseWorker = self;
4949
// eslint-disable-next-line @typescript-eslint/no-misused-promises
5050
worker.onmessage = async function (e) {
5151
if ('OffscreenCanvas' in globalThis) {
52-
const {
53-
id,
54-
bitmap,
55-
width,
56-
height,
57-
canvasWidth,
58-
canvasHeight,
59-
dataURLOptions,
60-
} = e.data;
52+
const { id, bitmap, width, height, dx, dy, dw, dh, dataURLOptions } =
53+
e.data;
6154

6255
const transparentBase64 = getTransparentBlobFor(
6356
width,
@@ -89,8 +82,10 @@ worker.onmessage = async function (e) {
8982
base64,
9083
width,
9184
height,
92-
canvasWidth,
93-
canvasHeight,
85+
dx,
86+
dy,
87+
dw,
88+
dh,
9489
});
9590
lastBlobMap.set(id, base64);
9691
} else {

packages/types/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
"dev": "vite",
1313
"check-types": "tsc -noEmit",
1414
"lint": "yarn eslint src/**/*.ts",
15-
"typegen": "vite build"
15+
"typegen": "tsc --emitDeclarationOnly -d",
16+
"build": "vite build"
1617
},
1718
"homepage": "https://github.com/rrweb-io/rrweb/tree/main/packages/@rrweb/types#readme",
1819
"bugs": {
@@ -40,8 +41,7 @@
4041
],
4142
"devDependencies": {
4243
"typescript": "^4.7.3",
43-
"vite": "^3.2.0-beta.2",
44-
"vite-plugin-dts": "^1.6.6"
44+
"vite": "^3.2.6"
4545
},
4646
"dependencies": {
4747
"@highlight-run/rrweb-snapshot": "workspace:*"

0 commit comments

Comments
 (0)