Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
42009f3
inline stylesheets when loaded
Juice10 May 31, 2022
b9ddfae
set empty link elements to loaded by default
Juice10 Jun 1, 2022
4c10804
Clean up stylesheet manager
Juice10 Jun 1, 2022
e9f8e06
Remove attribute mutation code
Juice10 Jun 2, 2022
c11e052
Update packages/rrweb/test/record.test.ts
Juice10 Jun 7, 2022
244a26a
Update packages/rrweb/test/record.test.ts
Juice10 Jun 7, 2022
1edb1ea
Update packages/rrweb/test/record.test.ts
Juice10 Jun 7, 2022
45bafbd
Merge branch 'master' of https://github.com/rrweb-io/rrweb into seria…
Juice10 Jun 7, 2022
401c503
Update packages/rrweb/scripts/repl.js
Juice10 Jun 7, 2022
2ec406b
Update packages/rrweb/test/record.test.ts
Juice10 Jun 7, 2022
2e8492e
Update packages/rrweb/src/record/index.ts
Juice10 Jun 7, 2022
217bd7c
Add todo
Juice10 Jun 7, 2022
c0371f0
Move require out of time sensitive assert
Juice10 Jun 7, 2022
fd38d43
Merge branch 'serialize-stylesheet-contents' of https://github.com/rr…
Juice10 Jun 7, 2022
dee14a6
Add waitForRAF, its more reliable than waitForTimeout
Juice10 Jun 7, 2022
1965152
Remove flaky tests
Juice10 Jun 7, 2022
8ff7bc6
Add recording stylesheets in iframes
Juice10 Jun 7, 2022
d5a83be
Remove variability from flaky test
Juice10 Jun 7, 2022
7e3a1a8
Make test more robust
Juice10 Jun 7, 2022
8c7a38f
Fix naming
Juice10 Jun 27, 2022
6878711
Merge branch 'master' of https://github.com/rrweb-io/rrweb into seria…
Juice10 Jun 27, 2022
d2cb411
Add test cases for inlineImages
Juice10 Jun 29, 2022
18c4475
Add test cases for inlineImages
Juice10 Jun 29, 2022
9efecfe
Merge branch 'inline-image-test-cases' of https://github.com/rrweb-io…
Juice10 Jun 30, 2022
da245bd
Record iframe mutations cross page
Juice10 Jun 30, 2022
be618d3
Test: should record images inside iframe with blob url after iframe w…
Juice10 Jun 30, 2022
9fc54b4
Merge branch 'master' of https://github.com/rrweb-io/rrweb into inlin…
Juice10 Jul 1, 2022
d63a529
Handle negative ids in rrdom correctly
Juice10 Jul 1, 2022
83102f0
Update packages/rrdom/src/diff.ts
Juice10 Jul 25, 2022
f872f76
Merge branch 'master' into rrdom-negative-ids
Juice10 Jul 25, 2022
3cabbf7
Start unserialized nodes at -2
Juice10 Jul 25, 2022
3a8ce08
Set unserialized id starting number at -2
Juice10 Jul 25, 2022
14179d6
Remove duplication
Juice10 Jul 25, 2022
7317ac3
Use turbo instead of lerna
Juice10 Jul 26, 2022
7cbb307
Skip benchmark as it is unreliable when executed in parallel
Juice10 Jul 26, 2022
eed8174
Strip port number from serialization, it can vary
Juice10 Jul 26, 2022
b43fafb
Add settimeout to virtual dom test
Juice10 Jul 26, 2022
c093928
Remove console.log and refactor blob:url serialization
Juice10 Jul 26, 2022
23c8711
Include references in tsconfig to indicate which monorepo packages ar…
Juice10 Jul 26, 2022
5006ed4
Add stream setup
Juice10 Jul 26, 2022
9291a05
Migrate project to es module
Juice10 Jul 27, 2022
f66b1c2
Add reference to rrweb from rrdom
Juice10 Jul 28, 2022
9599257
Move jest config to ESM
Juice10 Jul 28, 2022
bb1c5c5
Setup basic WebRTC canvas streaming
Juice10 Jul 28, 2022
03b6b28
Cleanup and refactor WebRTC streaming
Juice10 Jul 28, 2022
48a4ad3
Remove ? which isn't propper javascript
Juice10 Aug 22, 2022
05300fd
Yarn lock
Juice10 Aug 22, 2022
9be9176
Remove webrtc code from rrweb
Juice10 Aug 25, 2022
9925f07
Add plugin hooks
Juice10 Aug 25, 2022
7b768c7
Expose plugins with server
Juice10 Aug 25, 2022
678bd60
Use unminified version for tests
Juice10 Aug 25, 2022
9df08ea
Don't include simple-peer in rrweb main project
Juice10 Aug 25, 2022
e913c03
Add canvas webrtc plugin
Juice10 Aug 25, 2022
17164b2
Merge branch 'master' into live-stream
Juice10 Aug 25, 2022
22a5c1d
ignore tsconfig.tsbuildinfo
Juice10 Aug 25, 2022
b2065d6
Cleanup unused code
Juice10 Aug 25, 2022
41e3775
type definition files are no longer committed
Juice10 Aug 25, 2022
5ea10d7
Devtools off by default
Juice10 Aug 25, 2022
0d807e0
Extract .css into its own file
Juice10 Aug 25, 2022
43a73ed
Refactor plugin apis and fix multi canvas streaming support
Juice10 Aug 26, 2022
da14368
Add readme to rrweb canvas webrtc plugin
Juice10 Aug 26, 2022
9d950fa
Reference canvas-webrtc plugin in documentation
Juice10 Aug 26, 2022
39b6d22
Forbidden non-null assertion
Juice10 Aug 26, 2022
bc0bf13
Remove linting of each project, yarn lint:report will do this
Juice10 Aug 26, 2022
29964f5
Remove test code
Juice10 Aug 26, 2022
28c4744
Cut down line length
Juice10 Aug 26, 2022
fb0be78
Merge branch 'master' into live-stream
YunFeng0817 Sep 13, 2022
2919a20
fix CI failure and improve the zh_CN doc
YunFeng0817 Sep 13, 2022
c65bef0
Update packages/rrweb/src/plugins/canvas-webrtc/replay/index.ts
Juice10 Sep 14, 2022
0fbecb9
Cleaner styling of replay
Juice10 Sep 14, 2022
1e83835
Clean up stream.js based on @Mark-Fenng's feedback
Juice10 Sep 14, 2022
20f5f62
Remove duplicate send
Juice10 Sep 14, 2022
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
Prev Previous commit
Next Next commit
Cleanup and refactor WebRTC streaming
  • Loading branch information
Juice10 committed Jul 28, 2022
commit 03b6b28ee9af80ac777c447d85a53a07ddf3d99d
70 changes: 40 additions & 30 deletions packages/rrweb/scripts/stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { fileURLToPath } from 'url';
import { startServer, getServerURL } from './utils.js';

// Turn on devtools for debugging:
const devtools = false;
const devtools = true;
const defaultURL = 'https://webglsamples.org/';

const __filename = fileURLToPath(import.meta.url);
Expand All @@ -24,38 +24,39 @@ function getCode() {
}

async function startRecording(page, serverURL) {
page.on('recording - console', (msg) => console.log('PAGE LOG:', msg.text()));

await page.evaluate((serverURL) => {
const el = document.createElement('script');
el.addEventListener('load', () => {
const win = window;
win.__IS_RECORDING__ = true;
win.events = [];
win.rrweb?.record({
emit: (event) => {
win.events?.push(event);
win._replLog?.(event);
},
recordCanvas: true,
collectFonts: true,
inlineImages: true,
sampling: {
canvas: 'webrtc',
},
try {
await page.evaluate((serverURL) => {
const el = document.createElement('script');
el.addEventListener('load', () => {
const win = window;
win.__IS_RECORDING__ = true;
win.events = [];
window.record = win.rrweb.record;
window.record({
emit: (event) => {
win.events?.push(event);
win._captureEvent?.(event);
},
recordCanvas: true,
collectFonts: true,
inlineImages: true,
sampling: {
canvas: 'webrtc',
},
});
});
});
el.src = `${serverURL}/rrweb.js`;
document.head.appendChild(el);
}, serverURL);
el.src = `${serverURL}/rrweb.js`;
document.head.appendChild(el);
}, serverURL);
} catch (error) {
console.error(error);
}
}

async function startReplay(page, serverURL, recordedPage) {
page.on('replay - console', (msg) => console.log('PAGE LOG:', msg.text()));

await page.exposeFunction('_signal', (signal) => {
recordedPage.evaluate((signal) => {
window.p.signal(signal);
await page.exposeFunction('_signal', async (signal) => {
await recordedPage.evaluate((signal) => {
window.record.webRTCSignal(signal);
}, signal);
});

Expand All @@ -65,6 +66,9 @@ async function startReplay(page, serverURL, recordedPage) {
window.replayer = new rrweb.Replayer([], {
UNSAFE_replayCanvas: true,
liveMode: true,
webRTCSignalCallback: async (data) => {
await _signal(JSON.stringify(data));
},
});
window.replayer.startLive();
});
Expand Down Expand Up @@ -181,6 +185,12 @@ void (async () => {
// disables content security policy which enables us to insert rrweb as a script tag
await recordedPage.setBypassCSP(true);

replayerPage.on('console', (msg) =>
console.log('REPLAY PAGE LOG:', msg.text()),
);
recordedPage.on('console', (msg) =>
console.log('RECORD PAGE LOG:', msg.text()),
);
await startReplay(replayerPage, serverURL, recordedPage);

await Promise.all([
Expand All @@ -195,7 +205,7 @@ void (async () => {

if (!replayerPage) throw new Error('No replayer page found');

await recordedPage.exposeFunction('_replLog', (event) => {
await recordedPage.exposeFunction('_captureEvent', (event) => {
replayerPage.evaluate((event) => {
window.replayer?.addEvent(event);
}, event);
Expand Down
12 changes: 11 additions & 1 deletion packages/rrweb/src/record/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ function wrapEvent(e: event): eventWithTime {
let wrappedEmit!: (e: eventWithTime, isCheckout?: boolean) => void;

let takeFullSnapshot!: (isCheckout?: boolean) => void;
let canvasManager!: CanvasManager;

const mirror = createMirror();
function record<T = eventWithTime>(
Expand Down Expand Up @@ -221,7 +222,7 @@ function record<T = eventWithTime>(
mutationCb: wrappedMutationEmit,
});

const canvasManager = new CanvasManager({
canvasManager = new CanvasManager({
recordCanvas,
mutationCb: wrappedCanvasMutationEmit,
win: window,
Expand Down Expand Up @@ -529,6 +530,15 @@ record.addCustomEvent = <T>(tag: string, payload: T) => {
);
};

/**
* Insert response to an WebRTC Offer to complete the WebRTC connection
*
* @param signal - WebRTC signal supplied by the replayer's `webRTCSignalCallback` function
*/
record.webRTCSignal = (signal: RTCSessionDescriptionInit) => {
canvasManager.webRTCSignalCallback(signal);
};

record.freezePage = () => {
mutationBuffers.forEach((buf) => buf.freeze());
};
Expand Down
87 changes: 60 additions & 27 deletions packages/rrweb/src/record/observers/canvas/canvas-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
IWindow,
listenerHandler,
CanvasArg,
WebRTCDataChannel,
} from '../../../types';
import { CanvasContext } from '../../../types';
import initCanvas2DMutationObserver from './2d';
Expand Down Expand Up @@ -198,31 +199,68 @@ export class CanvasManager {
};
}

// private sdpOffer: RTCSessionDescriptionInit;
private sdpOffer: RTCSessionDescriptionInit;
private peer: SimplePeer.Instance;

private setupPeer(id: number, stream: MediaStream) {
console.log('setupPeer', id);
const p = new SimplePeer({
initiator: true,
stream,
trickle: false, // only send one WebRTC offer per canvas
});
public webRTCSignalCallback(signal: RTCSessionDescriptionInit) {
this.peer.signal(signal);
}

(window as any).p = p;
private startStream(id: number, stream: MediaStream) {
const data: WebRTCDataChannel = {
id,
};
this.peer.send(JSON.stringify(data));
this.peer.addStream(stream);
this.peer.send(JSON.stringify(data));
}

p.on('signal', (data: RTCSessionDescriptionInit) => {
this.mutationCb({
id,
type: CanvasContext.WebRTC,
msg: data,
stream,
private peerConnected = false;
private idSentSet: Set<number> = new Set();
private setupPeer(
id: number,
stream: MediaStream,
streamMap: Map<number, MediaStream>,
) {
if (!this.peer) {
this.peer = new SimplePeer({
initiator: true,
// trickle: false, // only create one WebRTC offer per session
});
});

p.on('connect', () => {
// eslint-disable-next-line prefer-rest-params
console.log('connected', arguments);
});
this.peer.on('error', (err) => {
console.error('record peer error', err);
});

this.peer.on('signal', (data: RTCSessionDescriptionInit) => {
this.sdpOffer = data;
this.mutationCb({
id,
type: CanvasContext.WebRTC,
msg: this.sdpOffer,
stream,
});
this.idSentSet.add(id);
});

this.peer.on('connect', () => {
this.peerConnected = true;
for (const [id, stream] of streamMap) {
this.startStream(id, stream);
if (!this.idSentSet.has(id)) {
this.mutationCb({
id,
type: CanvasContext.WebRTC,
msg: this.sdpOffer,
stream,
});
this.idSentSet.add(id);
}
}
});
} else if (this.sdpOffer && this.peerConnected) {
this.startStream(id, stream);
}
}

private initCanvasWebRTCObserver(win: IWindow, blockClass: blockClass) {
Expand All @@ -235,15 +273,10 @@ export class CanvasManager {
.querySelectorAll(`canvas:not(.${blockClass as string} *)`)
.forEach((canvas: HTMLCanvasElement) => {
const id = this.mirror.getId(canvas);
if (streamMap.has(id)) return;

// streamMap.set(id, (null as unknown) as MediaStream);
// setTimeout(() => {
console.log('captureStream', canvas, canvas.width, canvas.height);
if (id === -1 || streamMap.has(id)) return;
const stream = canvas.captureStream();
streamMap.set(id, stream);
this.setupPeer(id, stream);
// }, 2000);
this.setupPeer(id, stream, streamMap);
});
rafId = requestAnimationFrame(captureStreams);
};
Expand Down
90 changes: 57 additions & 33 deletions packages/rrweb/src/replay/canvas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
canvasMutationCommand,
canvasMutationData,
canvasMutationParam,
WebRTCDataChannel,
} from '../../types';
import webglMutation from './webgl';
import canvas2DMutation from './2d';
Expand All @@ -16,20 +17,29 @@ export default async function canvasMutation({
imageMap,
canvasEventMap,
errorHandler,
mirror,
webRTCSignalCallback,
}: {
event: Parameters<Replayer['applyIncremental']>[0];
mutation: canvasMutationData;
target: HTMLCanvasElement;
imageMap: Replayer['imageMap'];
canvasEventMap: Replayer['canvasEventMap'];
errorHandler: Replayer['warnCanvasMutationFailed'];
mirror: Replayer['mirror'];
webRTCSignalCallback: null | ((signal: RTCSessionDescriptionInit) => void);
}): Promise<void> {
try {
console.log('canvasMutation', mutation);
const precomputedMutation: canvasMutationParam =
canvasEventMap.get(event) || mutation;

if (precomputedMutation.type === CanvasContext.WebRTC) {
return setupWebRTC(precomputedMutation, target);
if (!webRTCSignalCallback)
throw new Error(
'webRTCSignalCallback is null, webRTC events will be ignored and Canvas or Video elements will be empty',
);
return setupWebRTC(precomputedMutation, webRTCSignalCallback, mirror);
}

const commands: canvasMutationCommand[] =
Expand Down Expand Up @@ -125,41 +135,55 @@ function startStream(
}
}

let peer: SimplePeer.Instance | null = null;
let streamId: number;
function setupWebRTC(
mutation: canvasMutationParam & {
type: CanvasContext.WebRTC;
},
target: HTMLCanvasElement | HTMLVideoElement,
// mirror: Replayer['mirror'],
webRTCSignalCallback: (signal: RTCSessionDescriptionInit) => void,
mirror: Replayer['mirror'],
) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const p = ((window as any).p = new SimplePeer({
initiator: false,
trickle: false,
}));

p.on('error', (err: Error) => console.log('error', err));

p.on('signal', (data: SimplePeer.SignalData) => {
console.log('SIGNAL', JSON.stringify(data));
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
_signal(JSON.stringify(data));
});

p.on('connect', () => {
console.log('CONNECT');
});

// p.on('data', (data) => {
// console.log('data: ', data);
// });

p.on('stream', (stream: MediaStream) => {
// got remote video stream, now let's show it in a video or canvas element
startStream(target, stream);
});

p.signal(mutation.msg);
if (!peer) {
peer = new SimplePeer({
initiator: false,
// trickle: false,
});

peer.on('error', (err: Error) => {
peer = null;
console.log('error', err);
});

peer.on('close', () => {
peer = null;
console.log('closing');
});

peer.on('signal', (data: RTCSessionDescriptionInit) => {
webRTCSignalCallback(data);
});

peer.on('connect', () => {
console.log('connect');
});

peer.on('data', (data: SimplePeer.SimplePeerData) => {
try {
const json = JSON.parse(data as string) as WebRTCDataChannel;
streamId = json.id;
} catch (error) {
console.error('Could not parse data', error);
}
});

peer.on('stream', (stream: MediaStream) => {
// got remote video stream, now let's show it in a video or canvas element
const target = mirror.getNode(streamId) as
| HTMLCanvasElement
| HTMLVideoElement;
startStream(target, stream);
});
}
peer.signal(mutation.msg);
}
Loading