-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
34 changed files
with
479 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
declare module 'rrweb/es/rrweb/packages/rrweb/src/replay/canvas' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
94 changes: 94 additions & 0 deletions
94
frontend/src/scenes/session-recordings/player/rrweb/canvas/canvas-plugin.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import { CanvasArg, canvasMutationData, canvasMutationParam, eventWithTime } from '@rrweb/types' | ||
import { EventType, IncrementalSource, Replayer } from 'rrweb' | ||
import { canvasMutation } from 'rrweb/es/rrweb/packages/rrweb/src/replay/canvas' | ||
import { ReplayPlugin } from 'rrweb/typings/types' | ||
|
||
import { deserializeCanvasArg } from './deserialize-canvas-args' | ||
|
||
export const CanvasReplayerPlugin = (events: eventWithTime[]): ReplayPlugin => { | ||
const canvases = new Map<number, HTMLCanvasElement>([]) | ||
const containers = new Map<number, HTMLImageElement>([]) | ||
const imageMap = new Map<eventWithTime | string, HTMLImageElement>() | ||
const canvasEventMap = new Map<eventWithTime | string, canvasMutationParam>() | ||
|
||
const deserializeAndPreloadCanvasEvents = async (data: canvasMutationData, event: eventWithTime): Promise<void> => { | ||
if (!canvasEventMap.has(event)) { | ||
const status = { isUnchanged: true } | ||
|
||
if ('commands' in data) { | ||
const commands = await Promise.all( | ||
data.commands.map(async (c) => { | ||
const args = await Promise.all( | ||
(c.args as CanvasArg[]).map(deserializeCanvasArg(imageMap, null, status)) | ||
) | ||
return { ...c, args } | ||
}) | ||
) | ||
if (status.isUnchanged === false) { | ||
canvasEventMap.set(event, { ...data, commands }) | ||
} | ||
} else { | ||
const args = await Promise.all( | ||
(data.args as CanvasArg[]).map(deserializeCanvasArg(imageMap, null, status)) | ||
) | ||
if (status.isUnchanged === false) { | ||
canvasEventMap.set(event, { ...data, args }) | ||
} | ||
} | ||
} | ||
} | ||
|
||
const cloneCanvas = (id: number, node: HTMLCanvasElement): HTMLCanvasElement => { | ||
const cloneNode = node.cloneNode() as HTMLCanvasElement | ||
canvases.set(id, cloneNode) | ||
document.adoptNode(cloneNode) | ||
return cloneNode | ||
} | ||
|
||
const promises: Promise<any>[] = [] | ||
for (const event of events) { | ||
if (event.type === EventType.IncrementalSnapshot && event.data.source === IncrementalSource.CanvasMutation) { | ||
promises.push(deserializeAndPreloadCanvasEvents(event.data, event)) | ||
} | ||
} | ||
|
||
return { | ||
onBuild: (node, { id }) => { | ||
if (!node) { | ||
return | ||
} | ||
|
||
if (node.nodeName === 'CANVAS' && node.nodeType === 1) { | ||
const el = containers.get(id) || document.createElement('img') | ||
;(node as HTMLCanvasElement).appendChild(el) | ||
containers.set(id, el) | ||
} | ||
}, | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-misused-promises | ||
handler: async (e: eventWithTime, _isSync: boolean, { replayer }: { replayer: Replayer }) => { | ||
if (e.type === EventType.IncrementalSnapshot && e.data.source === IncrementalSource.CanvasMutation) { | ||
const source = replayer.getMirror().getNode(e.data.id) | ||
const target = | ||
canvases.get(e.data.id) || (source && cloneCanvas(e.data.id, source as HTMLCanvasElement)) | ||
|
||
if (!target) { | ||
return | ||
} | ||
|
||
await canvasMutation({ | ||
event: e, | ||
mutation: e.data, | ||
target: target, | ||
imageMap, | ||
canvasEventMap, | ||
}) | ||
|
||
const img = containers.get(e.data.id) | ||
if (img) { | ||
img.src = target.toDataURL() | ||
} | ||
} | ||
}, | ||
} as ReplayPlugin | ||
} |
68 changes: 68 additions & 0 deletions
68
frontend/src/scenes/session-recordings/player/rrweb/canvas/deserialize-canvas-args.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import { CanvasArg } from '@rrweb/types' | ||
import { base64ArrayBuffer } from 'lib/utils' | ||
import { Replayer } from 'rrweb' | ||
|
||
type GLVarMap = Map<string, any[]> | ||
type CanvasContexts = CanvasRenderingContext2D | WebGLRenderingContext | WebGL2RenderingContext | ||
const webGLVarMap: Map<CanvasContexts, GLVarMap> = new Map() | ||
|
||
const variableListFor = (ctx: CanvasContexts, ctor: string): any[] => { | ||
let contextMap = webGLVarMap.get(ctx) | ||
if (!contextMap) { | ||
contextMap = new Map() | ||
webGLVarMap.set(ctx, contextMap) | ||
} | ||
if (!contextMap.has(ctor)) { | ||
contextMap.set(ctor, []) | ||
} | ||
|
||
return contextMap.get(ctor) as any[] | ||
} | ||
|
||
export const deserializeCanvasArg = ( | ||
imageMap: Replayer['imageMap'], | ||
ctx: CanvasContexts | null, | ||
preload?: { | ||
isUnchanged: boolean | ||
} | ||
): ((arg: CanvasArg) => Promise<any>) => { | ||
return async (arg: CanvasArg): Promise<any> => { | ||
if (arg && typeof arg === 'object' && 'rr_type' in arg) { | ||
if (preload) { | ||
preload.isUnchanged = false | ||
} | ||
if (arg.rr_type === 'ImageBitmap' && 'args' in arg) { | ||
const args = await deserializeCanvasArg(imageMap, ctx, preload)(arg.args) | ||
// eslint-disable-next-line prefer-spread | ||
return await createImageBitmap.apply(null, args) | ||
} | ||
if ('index' in arg) { | ||
if (preload || ctx === null) { | ||
return arg | ||
} | ||
const { rr_type: name, index } = arg | ||
return variableListFor(ctx, name)[index] | ||
} | ||
if ('args' in arg) { | ||
return arg | ||
} | ||
if ('base64' in arg) { | ||
return base64ArrayBuffer(arg.base64) | ||
} | ||
if ('src' in arg) { | ||
return arg | ||
} | ||
if ('data' in arg && arg.rr_type === 'Blob') { | ||
const blobContents = await Promise.all(arg.data.map(deserializeCanvasArg(imageMap, ctx, preload))) | ||
const blob = new Blob(blobContents, { | ||
type: arg.type, | ||
}) | ||
return blob | ||
} | ||
} else if (Array.isArray(arg)) { | ||
const result = await Promise.all(arg.map(deserializeCanvasArg(imageMap, ctx, preload))) | ||
return result | ||
} | ||
return arg | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.