-
-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add webgl recording and playback (#57)
- Loading branch information
John Pham
authored
Jan 4, 2022
1 parent
739c7fc
commit 9ab8e28
Showing
16 changed files
with
883 additions
and
117 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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import { INode } from '../../../snapshot'; | ||
import { | ||
blockClass, | ||
CanvasContext, | ||
canvasMutationCallback, | ||
IWindow, | ||
listenerHandler, | ||
Mirror, | ||
} from '../../../types'; | ||
import { hookSetter, isBlocked, patch } from '../../../utils'; | ||
|
||
export default function initCanvas2DMutationObserver( | ||
cb: canvasMutationCallback, | ||
win: IWindow, | ||
blockClass: blockClass, | ||
mirror: Mirror, | ||
): listenerHandler { | ||
const handlers: listenerHandler[] = []; | ||
const props2D = Object.getOwnPropertyNames( | ||
win.CanvasRenderingContext2D.prototype, | ||
); | ||
for (const prop of props2D) { | ||
try { | ||
if ( | ||
typeof win.CanvasRenderingContext2D.prototype[ | ||
prop as keyof CanvasRenderingContext2D | ||
] !== 'function' | ||
) { | ||
continue; | ||
} | ||
const restoreHandler = patch( | ||
win.CanvasRenderingContext2D.prototype, | ||
prop, | ||
function (original) { | ||
return function ( | ||
this: CanvasRenderingContext2D, | ||
...args: Array<unknown> | ||
) { | ||
if (!isBlocked((this.canvas as unknown) as INode, blockClass)) { | ||
setTimeout(() => { | ||
const recordArgs = [...args]; | ||
if (prop === 'drawImage') { | ||
if ( | ||
recordArgs[0] && | ||
recordArgs[0] instanceof HTMLCanvasElement | ||
) { | ||
const canvas = recordArgs[0]; | ||
const ctx = canvas.getContext('2d'); | ||
let imgd = ctx?.getImageData( | ||
0, | ||
0, | ||
canvas.width, | ||
canvas.height, | ||
); | ||
let pix = imgd?.data; | ||
recordArgs[0] = JSON.stringify(pix); | ||
} | ||
} | ||
cb({ | ||
id: mirror.getId((this.canvas as unknown) as INode), | ||
type: CanvasContext['2D'], | ||
property: prop, | ||
args: recordArgs, | ||
}); | ||
}, 0); | ||
} | ||
return original.apply(this, args); | ||
}; | ||
}, | ||
); | ||
handlers.push(restoreHandler); | ||
} catch { | ||
const hookHandler = hookSetter<CanvasRenderingContext2D>( | ||
win.CanvasRenderingContext2D.prototype, | ||
prop, | ||
{ | ||
set(v) { | ||
cb({ | ||
id: mirror.getId((this.canvas as unknown) as INode), | ||
type: CanvasContext['2D'], | ||
property: prop, | ||
args: [v], | ||
setter: true, | ||
}); | ||
}, | ||
}, | ||
); | ||
handlers.push(hookHandler); | ||
} | ||
} | ||
return () => { | ||
handlers.forEach((h) => h()); | ||
}; | ||
} |
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,35 @@ | ||
import { ICanvas, INode } from '../../../snapshot'; | ||
import { blockClass, IWindow, listenerHandler } from '../../../types'; | ||
import { isBlocked, patch } from '../../../utils'; | ||
|
||
export default function initCanvasContextObserver( | ||
win: IWindow, | ||
blockClass: blockClass, | ||
): listenerHandler { | ||
const handlers: listenerHandler[] = []; | ||
try { | ||
const restoreHandler = patch( | ||
win.HTMLCanvasElement.prototype, | ||
'getContext', | ||
function (original) { | ||
return function ( | ||
this: ICanvas, | ||
contextType: string, | ||
...args: Array<unknown> | ||
) { | ||
if (!isBlocked((this as unknown) as INode, blockClass)) { | ||
if (!('__context' in this)) | ||
(this as ICanvas).__context = contextType; | ||
} | ||
return original.apply(this, [contextType, ...args]); | ||
}; | ||
}, | ||
); | ||
handlers.push(restoreHandler); | ||
} catch { | ||
console.error('failed to patch HTMLCanvasElement.prototype.getContext'); | ||
} | ||
return () => { | ||
handlers.forEach((h) => h()); | ||
}; | ||
} |
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,125 @@ | ||
import { encode } from 'base64-arraybuffer'; | ||
import { SerializedWebGlArg } from '../../../types'; | ||
|
||
// from webgl-recorder: https://github.com/evanw/webgl-recorder/blob/bef0e65596e981ee382126587e2dcbe0fc7748e2/webgl-recorder.js#L50-L77 | ||
const webGLVars: Record<string, Array<any>> = {}; | ||
export function serializeArg(value: any): SerializedWebGlArg { | ||
if (value instanceof Array) { | ||
return value.map(serializeArg); | ||
} else if (value === null) { | ||
return value; | ||
} else if ( | ||
value instanceof Float32Array || | ||
value instanceof Float64Array || | ||
value instanceof Int32Array || | ||
value instanceof Uint32Array || | ||
value instanceof Uint8Array || | ||
value instanceof Uint16Array || | ||
value instanceof Int16Array || | ||
value instanceof Int8Array || | ||
value instanceof Uint8ClampedArray | ||
) { | ||
const name = value.constructor.name; | ||
return { | ||
rr_type: name, | ||
args: [Object.values(value)], | ||
}; | ||
} else if ( | ||
// SharedArrayBuffer disabled on most browsers due to spectre. | ||
// More info: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer/SharedArrayBuffer | ||
// value instanceof SharedArrayBuffer || | ||
value instanceof ArrayBuffer | ||
) { | ||
const name = value.constructor.name as 'ArrayBuffer'; | ||
const base64 = encode(value); | ||
|
||
return { | ||
rr_type: name, | ||
base64, | ||
}; | ||
} else if (value instanceof DataView) { | ||
const name = value.constructor.name; | ||
return { | ||
rr_type: name, | ||
args: [serializeArg(value.buffer), value.byteOffset, value.byteLength], | ||
}; | ||
} else if (value instanceof HTMLImageElement) { | ||
const name = value.constructor.name; | ||
const { src } = value; | ||
return { | ||
rr_type: name, | ||
src, | ||
}; | ||
} else if (value instanceof ImageData) { | ||
const name = value.constructor.name; | ||
return { | ||
rr_type: name, | ||
args: [serializeArg(value.data), value.width, value.height], | ||
}; | ||
} else if ( | ||
value instanceof WebGLActiveInfo || | ||
value instanceof WebGLBuffer || | ||
value instanceof WebGLFramebuffer || | ||
value instanceof WebGLProgram || | ||
value instanceof WebGLRenderbuffer || | ||
value instanceof WebGLShader || | ||
value instanceof WebGLShaderPrecisionFormat || | ||
value instanceof WebGLTexture || | ||
value instanceof WebGLUniformLocation || | ||
value instanceof WebGLVertexArrayObject || | ||
// In Chrome, value won't be an instanceof WebGLVertexArrayObject. | ||
(value && value.constructor.name == 'WebGLVertexArrayObjectOES') || | ||
typeof value === 'object' | ||
) { | ||
const name = value.constructor.name; | ||
const list = webGLVars[name] || (webGLVars[name] = []); | ||
let index = list.indexOf(value); | ||
|
||
if (index === -1) { | ||
index = list.length; | ||
list.push(value); | ||
} | ||
|
||
return { | ||
rr_type: name, | ||
index, | ||
}; | ||
} | ||
|
||
return value; | ||
} | ||
|
||
export const serializeArgs = (args: Array<any>) => { | ||
return [...args].map(serializeArg); | ||
}; | ||
|
||
export const saveWebGLVar = (value: any): number | void => { | ||
if ( | ||
!( | ||
value instanceof WebGLActiveInfo || | ||
value instanceof WebGLBuffer || | ||
value instanceof WebGLFramebuffer || | ||
value instanceof WebGLProgram || | ||
value instanceof WebGLRenderbuffer || | ||
value instanceof WebGLShader || | ||
value instanceof WebGLShaderPrecisionFormat || | ||
value instanceof WebGLTexture || | ||
value instanceof WebGLUniformLocation || | ||
value instanceof WebGLVertexArrayObject || | ||
// In Chrome, value won't be an instanceof WebGLVertexArrayObject. | ||
(value && value.constructor.name == 'WebGLVertexArrayObjectOES') || | ||
typeof value === 'object' | ||
) | ||
) | ||
return; | ||
|
||
const name = value.constructor.name; | ||
const list = webGLVars[name] || (webGLVars[name] = []); | ||
let index = list.indexOf(value); | ||
|
||
if (index === -1) { | ||
index = list.length; | ||
list.push(value); | ||
} | ||
return index; | ||
}; |
Oops, something went wrong.