mockrtc 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +29 -0
- package/LICENSE +201 -0
- package/README.md +290 -0
- package/dist/admin-bin.d.ts +2 -0
- package/dist/admin-bin.js +67 -0
- package/dist/admin-bin.js.map +1 -0
- package/dist/client/mockrtc-client.d.ts +12 -0
- package/dist/client/mockrtc-client.js +67 -0
- package/dist/client/mockrtc-client.js.map +1 -0
- package/dist/client/mockrtc-remote-peer.d.ts +15 -0
- package/dist/client/mockrtc-remote-peer.js +246 -0
- package/dist/client/mockrtc-remote-peer.js.map +1 -0
- package/dist/control-channel.d.ts +8 -0
- package/dist/control-channel.js +11 -0
- package/dist/control-channel.js.map +1 -0
- package/dist/handling/handler-builder.d.ts +138 -0
- package/dist/handling/handler-builder.js +164 -0
- package/dist/handling/handler-builder.js.map +1 -0
- package/dist/handling/handler-step-definitions.d.ts +63 -0
- package/dist/handling/handler-step-definitions.js +123 -0
- package/dist/handling/handler-step-definitions.js.map +1 -0
- package/dist/handling/handler-steps.d.ts +48 -0
- package/dist/handling/handler-steps.js +218 -0
- package/dist/handling/handler-steps.js.map +1 -0
- package/dist/main-browser.d.ts +9 -0
- package/dist/main-browser.js +26 -0
- package/dist/main-browser.js.map +1 -0
- package/dist/main.d.ts +58 -0
- package/dist/main.js +67 -0
- package/dist/main.js.map +1 -0
- package/dist/mockrtc-admin-plugin.d.ts +56 -0
- package/dist/mockrtc-admin-plugin.js +151 -0
- package/dist/mockrtc-admin-plugin.js.map +1 -0
- package/dist/mockrtc-admin-server.d.ts +7 -0
- package/dist/mockrtc-admin-server.js +18 -0
- package/dist/mockrtc-admin-server.js.map +1 -0
- package/dist/mockrtc-client.d.ts +12 -0
- package/dist/mockrtc-client.js +64 -0
- package/dist/mockrtc-client.js.map +1 -0
- package/dist/mockrtc-handler-builder.d.ts +15 -0
- package/dist/mockrtc-handler-builder.js +24 -0
- package/dist/mockrtc-handler-builder.js.map +1 -0
- package/dist/mockrtc-peer.d.ts +147 -0
- package/dist/mockrtc-peer.js +7 -0
- package/dist/mockrtc-peer.js.map +1 -0
- package/dist/mockrtc-remote-peer.d.ts +15 -0
- package/dist/mockrtc-remote-peer.js +234 -0
- package/dist/mockrtc-remote-peer.js.map +1 -0
- package/dist/mockrtc-server-peer.d.ts +29 -0
- package/dist/mockrtc-server-peer.js +145 -0
- package/dist/mockrtc-server-peer.js.map +1 -0
- package/dist/mockrtc-server.d.ts +14 -0
- package/dist/mockrtc-server.js +53 -0
- package/dist/mockrtc-server.js.map +1 -0
- package/dist/mockrtc.d.ts +25 -0
- package/dist/mockrtc.js +7 -0
- package/dist/mockrtc.js.map +1 -0
- package/dist/package.json +52 -0
- package/dist/server/mockrtc-admin-plugin.d.ts +17 -0
- package/dist/server/mockrtc-admin-plugin.js +163 -0
- package/dist/server/mockrtc-admin-plugin.js.map +1 -0
- package/dist/server/mockrtc-admin-server.d.ts +7 -0
- package/dist/server/mockrtc-admin-server.js +18 -0
- package/dist/server/mockrtc-admin-server.js.map +1 -0
- package/dist/server/mockrtc-server-peer.d.ts +24 -0
- package/dist/server/mockrtc-server-peer.js +141 -0
- package/dist/server/mockrtc-server-peer.js.map +1 -0
- package/dist/server/mockrtc-server.d.ts +14 -0
- package/dist/server/mockrtc-server.js +53 -0
- package/dist/server/mockrtc-server.js.map +1 -0
- package/dist/src/main.d.ts +1 -0
- package/dist/src/main.js +24 -0
- package/dist/src/main.js.map +1 -0
- package/dist/src/mockrtc-peer.d.ts +0 -0
- package/dist/src/mockrtc-peer.js +2 -0
- package/dist/src/mockrtc-peer.js.map +1 -0
- package/dist/src/mockrtc.d.ts +0 -0
- package/dist/src/mockrtc.js +65 -0
- package/dist/src/mockrtc.js.map +1 -0
- package/dist/webrtc/control-channel.d.ts +8 -0
- package/dist/webrtc/control-channel.js +11 -0
- package/dist/webrtc/control-channel.js.map +1 -0
- package/dist/webrtc/datachannel-stream.d.ts +25 -0
- package/dist/webrtc/datachannel-stream.js +86 -0
- package/dist/webrtc/datachannel-stream.js.map +1 -0
- package/dist/webrtc/mediatrack-stream.d.ts +29 -0
- package/dist/webrtc/mediatrack-stream.js +109 -0
- package/dist/webrtc/mediatrack-stream.js.map +1 -0
- package/dist/webrtc/mockrtc-connection.d.ts +14 -0
- package/dist/webrtc/mockrtc-connection.js +147 -0
- package/dist/webrtc/mockrtc-connection.js.map +1 -0
- package/dist/webrtc/peer-connection.d.ts +16 -0
- package/dist/webrtc/peer-connection.js +81 -0
- package/dist/webrtc/peer-connection.js.map +1 -0
- package/dist/webrtc/rtc-connection.d.ts +47 -0
- package/dist/webrtc/rtc-connection.js +370 -0
- package/dist/webrtc/rtc-connection.js.map +1 -0
- package/dist/webrtc-hooks.d.ts +30 -0
- package/dist/webrtc-hooks.js +224 -0
- package/dist/webrtc-hooks.js.map +1 -0
- package/karma.conf.ts +89 -0
- package/ngi-eu-footer.png +0 -0
- package/package.json +86 -0
- package/src/admin-bin.ts +57 -0
- package/src/client/mockrtc-client.ts +79 -0
- package/src/client/mockrtc-remote-peer.ts +286 -0
- package/src/handling/handler-builder.ts +215 -0
- package/src/handling/handler-step-definitions.ts +142 -0
- package/src/handling/handler-steps.ts +254 -0
- package/src/main-browser.ts +44 -0
- package/src/main.ts +109 -0
- package/src/mockrtc-peer.ts +176 -0
- package/src/mockrtc.ts +36 -0
- package/src/server/mockrtc-admin-plugin.ts +196 -0
- package/src/server/mockrtc-admin-server.ts +17 -0
- package/src/server/mockrtc-server-peer.ts +159 -0
- package/src/server/mockrtc-server.ts +53 -0
- package/src/webrtc/control-channel.ts +13 -0
- package/src/webrtc/datachannel-stream.ts +102 -0
- package/src/webrtc/mediatrack-stream.ts +135 -0
- package/src/webrtc/mockrtc-connection.ts +164 -0
- package/src/webrtc/rtc-connection.ts +420 -0
- package/src/webrtc-hooks.ts +245 -0
- package/test/integration/close-steps.spec.ts +39 -0
- package/test/integration/connection-setup.spec.ts +230 -0
- package/test/integration/echo-steps.spec.ts +88 -0
- package/test/integration/proxy.spec.ts +526 -0
- package/test/integration/send-steps.spec.ts +76 -0
- package/test/integration/smoke-test.spec.ts +100 -0
- package/test/integration/wait-steps.spec.ts +225 -0
- package/test/start-test-admin-server.ts +12 -0
- package/test/test-setup.ts +136 -0
- package/test/tsconfig.json +11 -0
- package/tsconfig.json +14 -0
- package/typedoc.json +19 -0
- package/wallaby.js +41 -0
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: 2022 Tim Perry <tim@httptoolkit.tech>
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
MockRTC,
|
|
8
|
+
expect,
|
|
9
|
+
waitForChannelOpen,
|
|
10
|
+
waitForChannelClose,
|
|
11
|
+
waitForState,
|
|
12
|
+
setupPerfectNegotiation
|
|
13
|
+
} from '../test-setup';
|
|
14
|
+
|
|
15
|
+
describe("When proxying WebRTC traffic", () => {
|
|
16
|
+
|
|
17
|
+
const mockRTC = MockRTC.getRemote();
|
|
18
|
+
|
|
19
|
+
beforeEach(() => mockRTC.start());
|
|
20
|
+
afterEach(() => mockRTC.stop());
|
|
21
|
+
|
|
22
|
+
it("should be able to transparently forward messages to a configured peer", async () => {
|
|
23
|
+
const remoteConn = new RTCPeerConnection();
|
|
24
|
+
const remotelyReceivedMessages: Array<string | Buffer> = [];
|
|
25
|
+
|
|
26
|
+
remoteConn.addEventListener('datachannel', ({ channel }) => {
|
|
27
|
+
channel.addEventListener('message', ({ data }) => remotelyReceivedMessages.push(data));
|
|
28
|
+
channel.send("remote message 1");
|
|
29
|
+
channel.send("remote message 2");
|
|
30
|
+
channel.send("remote message 3");
|
|
31
|
+
setTimeout(() => channel.close(), 100);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const mockPeer = await mockRTC.buildPeer()
|
|
35
|
+
.waitForNextMessage()
|
|
36
|
+
.send('Injected message')
|
|
37
|
+
.thenForwardTo(remoteConn);
|
|
38
|
+
|
|
39
|
+
// Create a data connection:
|
|
40
|
+
const localConn = new RTCPeerConnection();
|
|
41
|
+
|
|
42
|
+
const dataChannel = localConn.createDataChannel("dataChannel");
|
|
43
|
+
const locallyReceivedMessages: Array<string | Buffer> = [];
|
|
44
|
+
dataChannel.addEventListener('message', ({ data }) => locallyReceivedMessages.push(data));
|
|
45
|
+
|
|
46
|
+
const localOffer = await localConn.createOffer();
|
|
47
|
+
localConn.setLocalDescription(localOffer);
|
|
48
|
+
|
|
49
|
+
// Get the remote details for the mock peer:
|
|
50
|
+
const { answer } = await mockPeer.answerOffer(localOffer);
|
|
51
|
+
await localConn.setRemoteDescription(answer);
|
|
52
|
+
|
|
53
|
+
await waitForChannelOpen(dataChannel);
|
|
54
|
+
|
|
55
|
+
dataChannel.send('local message 1');
|
|
56
|
+
dataChannel.send('local message 2');
|
|
57
|
+
dataChannel.send('local message 3');
|
|
58
|
+
|
|
59
|
+
await waitForChannelClose(dataChannel);
|
|
60
|
+
|
|
61
|
+
expect(locallyReceivedMessages).to.deep.equal([
|
|
62
|
+
'Injected message', // Injected by thenSend step
|
|
63
|
+
'remote message 1',
|
|
64
|
+
'remote message 2',
|
|
65
|
+
'remote message 3'
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
expect(remotelyReceivedMessages).to.deep.equal([
|
|
69
|
+
// First message is captured by waitForNextMessage step
|
|
70
|
+
'local message 2',
|
|
71
|
+
'local message 3'
|
|
72
|
+
]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("should be able to transparently forward media to a configured peer", async () => {
|
|
76
|
+
const remoteConn = new RTCPeerConnection();
|
|
77
|
+
|
|
78
|
+
// Turn the remote's received tracks into readable streams of frames:
|
|
79
|
+
const receivedMediaStreamPromise = new Promise<ReadableStream<VideoFrame>>((resolve) => {
|
|
80
|
+
remoteConn.addEventListener('track', ({ track }) => {
|
|
81
|
+
const streamProcessor = new MediaStreamTrackProcessor({
|
|
82
|
+
track: track as MediaStreamVideoTrack
|
|
83
|
+
});
|
|
84
|
+
resolve(streamProcessor.readable);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const mockPeer = await mockRTC.buildPeer()
|
|
89
|
+
.thenForwardTo(remoteConn);
|
|
90
|
+
|
|
91
|
+
// Create a data connection that will send video:
|
|
92
|
+
const localConn = new RTCPeerConnection();
|
|
93
|
+
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
|
|
94
|
+
localConn.addTrack(stream.getTracks()[0], stream);
|
|
95
|
+
|
|
96
|
+
const localOffer = await localConn.createOffer();
|
|
97
|
+
localConn.setLocalDescription(localOffer);
|
|
98
|
+
|
|
99
|
+
// Get the remote details for the mock peer:
|
|
100
|
+
const { answer } = await mockPeer.answerOffer(localOffer);
|
|
101
|
+
await localConn.setRemoteDescription(answer);
|
|
102
|
+
|
|
103
|
+
// Check we receive the expected echoed video in the remote peer:
|
|
104
|
+
const localMedia = await receivedMediaStreamPromise;
|
|
105
|
+
const { value: localFrame } = await localMedia!.getReader().read();
|
|
106
|
+
expect(localFrame!.displayHeight).to.be.greaterThanOrEqual(240);
|
|
107
|
+
expect(localFrame!.displayWidth).to.be.greaterThanOrEqual(320);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should be able to transparently proxy messages to a dynamically provided peer, sending offer", async () => {
|
|
111
|
+
const remoteConn = new RTCPeerConnection();
|
|
112
|
+
const remotelyReceivedMessages: Array<string | Buffer> = [];
|
|
113
|
+
|
|
114
|
+
remoteConn.addEventListener('datachannel', ({ channel }) => {
|
|
115
|
+
channel.addEventListener('message', ({ data }) => remotelyReceivedMessages.push(data));
|
|
116
|
+
channel.send("remote message 1");
|
|
117
|
+
channel.send("remote message 2");
|
|
118
|
+
channel.send("remote message 3");
|
|
119
|
+
setTimeout(() => channel.close(), 100);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const mockPeer = await mockRTC.buildPeer()
|
|
123
|
+
.waitForNextMessage()
|
|
124
|
+
.send('Injected message')
|
|
125
|
+
.thenPassThrough();
|
|
126
|
+
|
|
127
|
+
// Create a local data connection:
|
|
128
|
+
const localConn = new RTCPeerConnection();
|
|
129
|
+
MockRTC.hookWebRTCConnection(localConn, mockPeer); // Automatically redirect traffic via mockPeer
|
|
130
|
+
|
|
131
|
+
const dataChannel = localConn.createDataChannel("dataChannel");
|
|
132
|
+
const locallyReceivedMessages: Array<string | Buffer> = [];
|
|
133
|
+
dataChannel.addEventListener('message', ({ data }) => locallyReceivedMessages.push(data));
|
|
134
|
+
|
|
135
|
+
// Create a local offer (which will be hooked automatically):
|
|
136
|
+
const localOffer = await localConn.createOffer();
|
|
137
|
+
localConn.setLocalDescription(localOffer);
|
|
138
|
+
|
|
139
|
+
// v-- Normally happens remotely, via signalling ---
|
|
140
|
+
remoteConn.setRemoteDescription(localOffer);
|
|
141
|
+
const remoteAnswer = await remoteConn.createAnswer();
|
|
142
|
+
remoteConn.setLocalDescription(remoteAnswer);
|
|
143
|
+
// ^-- Normally happens remotely, via signalling ---
|
|
144
|
+
|
|
145
|
+
// Accept the real remote answer, and start communicating:
|
|
146
|
+
localConn.setRemoteDescription(remoteAnswer);
|
|
147
|
+
|
|
148
|
+
await new Promise<void>((resolve) => dataChannel.onopen = () => resolve());
|
|
149
|
+
|
|
150
|
+
dataChannel.send('local message 1');
|
|
151
|
+
dataChannel.send('local message 2');
|
|
152
|
+
dataChannel.send('local message 3');
|
|
153
|
+
|
|
154
|
+
await new Promise((resolve) => dataChannel.addEventListener('close', resolve));
|
|
155
|
+
|
|
156
|
+
expect(locallyReceivedMessages).to.deep.equal([
|
|
157
|
+
'Injected message', // Injected by thenSend step
|
|
158
|
+
'remote message 1',
|
|
159
|
+
'remote message 2',
|
|
160
|
+
'remote message 3'
|
|
161
|
+
]);
|
|
162
|
+
|
|
163
|
+
expect(remotelyReceivedMessages).to.deep.equal([
|
|
164
|
+
// First message is captured by waitForNextMessage step
|
|
165
|
+
'local message 2',
|
|
166
|
+
'local message 3'
|
|
167
|
+
]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("should be able to transparently proxy messages to dynamically provided peer, receiving answer", async () => {
|
|
171
|
+
const remoteConn = new RTCPeerConnection();
|
|
172
|
+
remoteConn.createDataChannel("empty-channel"); // We need to create at least one channel/track to get an offer
|
|
173
|
+
const remotelyReceivedMessages: Array<string | Buffer> = [];
|
|
174
|
+
|
|
175
|
+
remoteConn.addEventListener('datachannel', ({ channel }) => {
|
|
176
|
+
channel.addEventListener('message', ({ data }) => remotelyReceivedMessages.push(data));
|
|
177
|
+
channel.send("remote message 1");
|
|
178
|
+
channel.send("remote message 2");
|
|
179
|
+
channel.send("remote message 3");
|
|
180
|
+
setTimeout(() => channel.close(), 100);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const mockPeer = await mockRTC.buildPeer()
|
|
184
|
+
.waitForNextMessage()
|
|
185
|
+
.send('Injected message')
|
|
186
|
+
.thenPassThrough();
|
|
187
|
+
|
|
188
|
+
// Remote connection starts first, sending us a real offer:
|
|
189
|
+
const remoteOffer = await remoteConn.createOffer();
|
|
190
|
+
remoteConn.setLocalDescription(remoteOffer);
|
|
191
|
+
|
|
192
|
+
// Create a local data connection:
|
|
193
|
+
const localConn = new RTCPeerConnection();
|
|
194
|
+
MockRTC.hookWebRTCConnection(localConn, mockPeer); // Automatically redirect traffic via mockPeer
|
|
195
|
+
|
|
196
|
+
const dataChannel = localConn.createDataChannel("dataChannel");
|
|
197
|
+
const channelOpenPromise = new Promise<void>((resolve) => dataChannel.onopen = () => resolve());
|
|
198
|
+
const locallyReceivedMessages: Array<string | Buffer> = [];
|
|
199
|
+
dataChannel.addEventListener('message', ({ data }) => locallyReceivedMessages.push(data));
|
|
200
|
+
|
|
201
|
+
// Receive the remote offer, use that locally to create an answer (this is all hooked):
|
|
202
|
+
await localConn.setRemoteDescription(remoteOffer);
|
|
203
|
+
const localAnswer = await localConn.createAnswer();
|
|
204
|
+
localConn.setLocalDescription(localAnswer);
|
|
205
|
+
|
|
206
|
+
// Signal the answer back to the real unhooked remote connection:
|
|
207
|
+
await remoteConn.setRemoteDescription(localAnswer);
|
|
208
|
+
|
|
209
|
+
await channelOpenPromise;
|
|
210
|
+
|
|
211
|
+
dataChannel.send('local message 1');
|
|
212
|
+
dataChannel.send('local message 2');
|
|
213
|
+
dataChannel.send('local message 3');
|
|
214
|
+
|
|
215
|
+
await new Promise((resolve) => dataChannel.addEventListener('close', resolve));
|
|
216
|
+
|
|
217
|
+
expect(locallyReceivedMessages).to.deep.equal([
|
|
218
|
+
'Injected message', // Injected by thenSend step
|
|
219
|
+
'remote message 1',
|
|
220
|
+
'remote message 2',
|
|
221
|
+
'remote message 3'
|
|
222
|
+
]);
|
|
223
|
+
|
|
224
|
+
expect(remotelyReceivedMessages).to.deep.equal([
|
|
225
|
+
// First message is captured by waitForNextMessage step
|
|
226
|
+
'local message 2',
|
|
227
|
+
'local message 3'
|
|
228
|
+
]);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("should be able to transparently proxy messages when hooking both ends of a connection", async () => {
|
|
232
|
+
const mockPeer = await mockRTC.buildPeer()
|
|
233
|
+
.waitForNextMessage()
|
|
234
|
+
.send('Injected message')
|
|
235
|
+
.thenPassThrough();
|
|
236
|
+
|
|
237
|
+
const remoteConn = new RTCPeerConnection();
|
|
238
|
+
MockRTC.hookWebRTCConnection(remoteConn, mockPeer); // Automatically redirect traffic via mockPeer
|
|
239
|
+
|
|
240
|
+
const remotelyReceivedMessages: Array<string | Buffer> = [];
|
|
241
|
+
|
|
242
|
+
// Like localChannel, We have to create an outgoing channel for the wait & send step.
|
|
243
|
+
remoteConn.createDataChannel("remote-channel").onopen = function () {
|
|
244
|
+
this.addEventListener('message', ({ data }) => remotelyReceivedMessages.push(data));
|
|
245
|
+
this.send('remote message 1'); // Required on an outgoing channel to pass waitForNextMessage
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// Remote listens for local's channel, sends replies, and closes
|
|
249
|
+
remoteConn.addEventListener('datachannel', ({ channel }) => {
|
|
250
|
+
channel.addEventListener('message', ({ data }) => remotelyReceivedMessages.push(data));
|
|
251
|
+
channel.send("remote message 2");
|
|
252
|
+
channel.send("remote message 3");
|
|
253
|
+
channel.send("remote message 4");
|
|
254
|
+
setTimeout(() => channel.close(), 100);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Remote connection starts first, sending a hooked offer:
|
|
258
|
+
const remoteOffer = await remoteConn.createOffer();
|
|
259
|
+
remoteConn.setLocalDescription(remoteOffer);
|
|
260
|
+
|
|
261
|
+
// We create a local data connection too:
|
|
262
|
+
const localConn = new RTCPeerConnection();
|
|
263
|
+
MockRTC.hookWebRTCConnection(localConn, mockPeer); // Automatically redirect traffic via mockPeer
|
|
264
|
+
|
|
265
|
+
const dataChannel = localConn.createDataChannel("localDataChannel");
|
|
266
|
+
const channelOpenPromise = new Promise<void>((resolve) => dataChannel.onopen = () => resolve());
|
|
267
|
+
const locallyReceivedMessages: Array<string | Buffer> = [];
|
|
268
|
+
dataChannel.addEventListener('message', ({ data }) => locallyReceivedMessages.push(data));
|
|
269
|
+
|
|
270
|
+
// Receive the remote offer, use that locally to create an answer (all hooked):
|
|
271
|
+
await localConn.setRemoteDescription(remoteOffer);
|
|
272
|
+
const localAnswer = await localConn.createAnswer();
|
|
273
|
+
localConn.setLocalDescription(localAnswer);
|
|
274
|
+
|
|
275
|
+
// Signal the answer back to the remote connection, which hooks this too:
|
|
276
|
+
await remoteConn.setRemoteDescription(localAnswer);
|
|
277
|
+
|
|
278
|
+
await channelOpenPromise;
|
|
279
|
+
|
|
280
|
+
dataChannel.send('local message 1');
|
|
281
|
+
dataChannel.send('local message 2');
|
|
282
|
+
dataChannel.send('local message 3');
|
|
283
|
+
|
|
284
|
+
await new Promise((resolve) => dataChannel.addEventListener('close', resolve));
|
|
285
|
+
|
|
286
|
+
expect(locallyReceivedMessages).to.deep.equal([
|
|
287
|
+
'Injected message', // Injected by send step
|
|
288
|
+
'remote message 2',
|
|
289
|
+
'remote message 3',
|
|
290
|
+
'remote message 4'
|
|
291
|
+
]);
|
|
292
|
+
|
|
293
|
+
expect(remotelyReceivedMessages).to.deep.equal([
|
|
294
|
+
'Injected message', // Injected by send step here too! Both peers are hooked
|
|
295
|
+
// Local message is eaten by waitForNextMessage
|
|
296
|
+
'local message 2',
|
|
297
|
+
'local message 3'
|
|
298
|
+
]);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("should be able to transparently proxy messages when hooking both ends of a perfect negotiation", async () => {
|
|
302
|
+
const mockPeer = await mockRTC.buildPeer()
|
|
303
|
+
.sleep(100)
|
|
304
|
+
.send('Injected message')
|
|
305
|
+
.thenPassThrough();
|
|
306
|
+
|
|
307
|
+
// Two hooked peers:
|
|
308
|
+
const remoteConn = new RTCPeerConnection();
|
|
309
|
+
MockRTC.hookWebRTCConnection(remoteConn, mockPeer);
|
|
310
|
+
const localConn = new RTCPeerConnection();
|
|
311
|
+
MockRTC.hookWebRTCConnection(localConn, mockPeer);
|
|
312
|
+
|
|
313
|
+
// A fake synchronous signalling channel:
|
|
314
|
+
const signaler1 = { send: (msg: any) => signaler2.onmessage(msg), onmessage: (msg: any) => {} };
|
|
315
|
+
const signaler2 = { send: (msg: any) => signaler1.onmessage(msg), onmessage: (msg: any) => {} };
|
|
316
|
+
|
|
317
|
+
// Do perfect negotiation in parallel to connect our two peers:
|
|
318
|
+
setupPerfectNegotiation(remoteConn, true, signaler1); // Polite
|
|
319
|
+
setupPerfectNegotiation(localConn, false, signaler2); // Impolite
|
|
320
|
+
|
|
321
|
+
const remotelyReceivedMessages: Array<string | Buffer> = [];
|
|
322
|
+
|
|
323
|
+
// Remote listens for local's channel, sends replies, and closes
|
|
324
|
+
remoteConn.addEventListener('datachannel', ({ channel }) => {
|
|
325
|
+
channel.addEventListener('message', ({ data }) => remotelyReceivedMessages.push(data));
|
|
326
|
+
channel.send("remote message 2");
|
|
327
|
+
channel.send("remote message 3");
|
|
328
|
+
channel.send("remote message 4");
|
|
329
|
+
setTimeout(() => channel.close(), 100);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const dataChannel = localConn.createDataChannel("localDataChannel");
|
|
333
|
+
const channelOpenPromise = new Promise<void>((resolve) => dataChannel.onopen = () => resolve());
|
|
334
|
+
const locallyReceivedMessages: Array<string | Buffer> = [];
|
|
335
|
+
dataChannel.addEventListener('message', ({ data }) => locallyReceivedMessages.push(data));
|
|
336
|
+
|
|
337
|
+
await channelOpenPromise;
|
|
338
|
+
|
|
339
|
+
dataChannel.send('local message 1');
|
|
340
|
+
dataChannel.send('local message 2');
|
|
341
|
+
dataChannel.send('local message 3');
|
|
342
|
+
|
|
343
|
+
await new Promise((resolve) => dataChannel.addEventListener('close', resolve));
|
|
344
|
+
|
|
345
|
+
expect(locallyReceivedMessages).to.deep.equal([
|
|
346
|
+
'Injected message', // Injected by send step
|
|
347
|
+
'remote message 2',
|
|
348
|
+
'remote message 3',
|
|
349
|
+
'remote message 4'
|
|
350
|
+
]);
|
|
351
|
+
|
|
352
|
+
expect(remotelyReceivedMessages).to.deep.equal([
|
|
353
|
+
'local message 1',
|
|
354
|
+
'local message 2',
|
|
355
|
+
'local message 3'
|
|
356
|
+
]);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("should be able to transparently proxy offered media through a hooked connection", async () => {
|
|
360
|
+
const mockPeer = await mockRTC.buildPeer()
|
|
361
|
+
.thenPassThrough();
|
|
362
|
+
|
|
363
|
+
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
|
|
364
|
+
|
|
365
|
+
// One real peer:
|
|
366
|
+
const remoteConn = new RTCPeerConnection();
|
|
367
|
+
|
|
368
|
+
// One hooked peer:
|
|
369
|
+
const localConn = new RTCPeerConnection();
|
|
370
|
+
MockRTC.hookWebRTCConnection(localConn, mockPeer);
|
|
371
|
+
|
|
372
|
+
// Both connections offer to send media:
|
|
373
|
+
remoteConn.addTrack(stream.getTracks()[0], stream);
|
|
374
|
+
localConn.addTrack(stream.getTracks()[0], stream);
|
|
375
|
+
|
|
376
|
+
// Turn incoming tracks into readable streams of frames:
|
|
377
|
+
const mediaStreamPromise = Promise.all([remoteConn, localConn].map((conn) => {
|
|
378
|
+
return new Promise<ReadableStream<VideoFrame>>((resolve) => {
|
|
379
|
+
conn.addEventListener('track', ({ track }) => {
|
|
380
|
+
const streamProcessor = new MediaStreamTrackProcessor({
|
|
381
|
+
track: track as MediaStreamVideoTrack
|
|
382
|
+
});
|
|
383
|
+
resolve(streamProcessor.readable);
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
}));
|
|
387
|
+
|
|
388
|
+
// Set up the connection
|
|
389
|
+
const localOffer = await localConn.createOffer(); // Hooked
|
|
390
|
+
localConn.setLocalDescription(localOffer); // Hooked
|
|
391
|
+
remoteConn.setRemoteDescription(localOffer);
|
|
392
|
+
const remoteAnswer = await remoteConn.createAnswer();
|
|
393
|
+
remoteConn.setLocalDescription(remoteAnswer);
|
|
394
|
+
localConn.setRemoteDescription(remoteAnswer); // Hooked
|
|
395
|
+
|
|
396
|
+
await waitForState(remoteConn, 'connected');
|
|
397
|
+
|
|
398
|
+
// Extract the first frame from each, once they arrive:
|
|
399
|
+
const [remoteMedia, localMedia] = await mediaStreamPromise;
|
|
400
|
+
const { value: remoteFrame } = await remoteMedia!.getReader().read();
|
|
401
|
+
const { value: localFrame } = await localMedia!.getReader().read();
|
|
402
|
+
|
|
403
|
+
// Check both peers receive video - using sizes from fake media when running headlessly:
|
|
404
|
+
expect(localFrame!.displayHeight).to.be.greaterThanOrEqual(240);
|
|
405
|
+
expect(localFrame!.displayWidth).to.be.greaterThanOrEqual(320);
|
|
406
|
+
expect(remoteFrame!.displayHeight).to.be.greaterThanOrEqual(240);
|
|
407
|
+
expect(remoteFrame!.displayWidth).to.be.greaterThanOrEqual(320);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it("should be able to transparently proxy answered media through a hooked connection", async () => {
|
|
411
|
+
const mockPeer = await mockRTC.buildPeer()
|
|
412
|
+
.thenPassThrough();
|
|
413
|
+
|
|
414
|
+
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
|
|
415
|
+
|
|
416
|
+
// One real peer:
|
|
417
|
+
const remoteConn = new RTCPeerConnection();
|
|
418
|
+
|
|
419
|
+
// One hooked peer:
|
|
420
|
+
const localConn = new RTCPeerConnection();
|
|
421
|
+
MockRTC.hookWebRTCConnection(localConn, mockPeer);
|
|
422
|
+
|
|
423
|
+
// Both connections offer to send media:
|
|
424
|
+
remoteConn.addTrack(stream.getTracks()[0], stream);
|
|
425
|
+
localConn.addTrack(stream.getTracks()[0], stream);
|
|
426
|
+
|
|
427
|
+
// Turn incoming tracks into readable streams of frames:
|
|
428
|
+
const mediaStreamPromise = Promise.all([remoteConn, localConn].map((conn) => {
|
|
429
|
+
return new Promise<ReadableStream<VideoFrame>>((resolve) => {
|
|
430
|
+
conn.addEventListener('track', ({ track }) => {
|
|
431
|
+
const streamProcessor = new MediaStreamTrackProcessor({
|
|
432
|
+
track: track as MediaStreamVideoTrack
|
|
433
|
+
});
|
|
434
|
+
resolve(streamProcessor.readable);
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
}));
|
|
438
|
+
|
|
439
|
+
// Set up the connection
|
|
440
|
+
const remoteOffer = await remoteConn.createOffer();
|
|
441
|
+
remoteConn.setLocalDescription(remoteOffer);
|
|
442
|
+
localConn.setRemoteDescription(remoteOffer); // Hooked
|
|
443
|
+
const localAnswer = await localConn.createAnswer(); // Hooked
|
|
444
|
+
localConn.setLocalDescription(localAnswer); // Hooked
|
|
445
|
+
remoteConn.setRemoteDescription(localAnswer);
|
|
446
|
+
|
|
447
|
+
await waitForState(remoteConn, 'connected');
|
|
448
|
+
|
|
449
|
+
// Extract the first frame from each, once they arrive:
|
|
450
|
+
const [remoteMedia, localMedia] = await mediaStreamPromise;
|
|
451
|
+
const { value: remoteFrame } = await remoteMedia!.getReader().read();
|
|
452
|
+
const { value: localFrame } = await localMedia!.getReader().read();
|
|
453
|
+
|
|
454
|
+
// Check both peers receive video - using sizes from fake media when running headlessly:
|
|
455
|
+
expect(localFrame!.displayHeight).to.be.greaterThanOrEqual(240);
|
|
456
|
+
expect(localFrame!.displayWidth).to.be.greaterThanOrEqual(320);
|
|
457
|
+
expect(remoteFrame!.displayHeight).to.be.greaterThanOrEqual(240);
|
|
458
|
+
expect(remoteFrame!.displayWidth).to.be.greaterThanOrEqual(320);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it("should be able to transparently proxy media when hooking both ends of a perfect negotiation", async () => {
|
|
462
|
+
const mockPeer = await mockRTC.buildPeer()
|
|
463
|
+
.thenPassThrough();
|
|
464
|
+
|
|
465
|
+
const stream1 = await navigator.mediaDevices.getUserMedia({ video: true });
|
|
466
|
+
const stream2 = await navigator.mediaDevices.getUserMedia({ video: true });
|
|
467
|
+
|
|
468
|
+
// Two hooked peers:
|
|
469
|
+
const remoteConn = new RTCPeerConnection();
|
|
470
|
+
MockRTC.hookWebRTCConnection(remoteConn, mockPeer);
|
|
471
|
+
const localConn = new RTCPeerConnection();
|
|
472
|
+
MockRTC.hookWebRTCConnection(localConn, mockPeer);
|
|
473
|
+
|
|
474
|
+
// A fake synchronous signalling channel:
|
|
475
|
+
const signaler1 = { send: (msg: any) => signaler2.onmessage(msg), onmessage: (msg: any) => {} };
|
|
476
|
+
const signaler2 = { send: (msg: any) => signaler1.onmessage(msg), onmessage: (msg: any) => {} };
|
|
477
|
+
|
|
478
|
+
// Do perfect negotiation in parallel to connect our two peers:
|
|
479
|
+
setupPerfectNegotiation(localConn, true, signaler1); // Polite
|
|
480
|
+
setTimeout(() => {
|
|
481
|
+
// Add a tiny delay to deal with https://bugs.chromium.org/p/chromium/issues/detail?id=1315611
|
|
482
|
+
setupPerfectNegotiation(remoteConn, false, signaler2); // Impolite
|
|
483
|
+
}, 1);
|
|
484
|
+
|
|
485
|
+
// Both peers send user media as a video stream:
|
|
486
|
+
([
|
|
487
|
+
[localConn, stream1, 'local'],
|
|
488
|
+
[remoteConn, stream2, 'remote']
|
|
489
|
+
] as const).forEach(([conn, stream, name]) => {
|
|
490
|
+
const track = stream.getTracks()[0];
|
|
491
|
+
conn.addTrack(track, stream);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// Turn incoming tracks into readable streams of frames:
|
|
495
|
+
const mediaStreamPromise = Promise.all([
|
|
496
|
+
localConn,
|
|
497
|
+
remoteConn
|
|
498
|
+
].map((conn, i) => {
|
|
499
|
+
return new Promise<ReadableStream<VideoFrame>>((resolve) => {
|
|
500
|
+
conn.addEventListener('track', ({ track }) => {
|
|
501
|
+
const streamProcessor = new MediaStreamTrackProcessor({
|
|
502
|
+
track: track as MediaStreamVideoTrack
|
|
503
|
+
});
|
|
504
|
+
resolve(streamProcessor.readable);
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
}));
|
|
508
|
+
|
|
509
|
+
await waitForState(remoteConn, 'connected');
|
|
510
|
+
|
|
511
|
+
// Extract the first frame from each, once they arrive:
|
|
512
|
+
const [
|
|
513
|
+
localMedia,
|
|
514
|
+
remoteMedia
|
|
515
|
+
] = await mediaStreamPromise;
|
|
516
|
+
const { value: remoteFrame } = await remoteMedia!.getReader().read();
|
|
517
|
+
const { value: localFrame } = await localMedia!.getReader().read();
|
|
518
|
+
|
|
519
|
+
// Check both peers receive video - using sizes from fake media when running headlessly:
|
|
520
|
+
expect(localFrame!.displayHeight).to.be.greaterThanOrEqual(240);
|
|
521
|
+
expect(localFrame!.displayWidth).to.be.greaterThanOrEqual(320);
|
|
522
|
+
expect(remoteFrame!.displayHeight).to.be.greaterThanOrEqual(240);
|
|
523
|
+
expect(remoteFrame!.displayWidth).to.be.greaterThanOrEqual(320);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: 2022 Tim Perry <tim@httptoolkit.tech>
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
MockRTC,
|
|
8
|
+
expect,
|
|
9
|
+
waitForChannelClose
|
|
10
|
+
} from '../test-setup';
|
|
11
|
+
|
|
12
|
+
describe("Send steps", function () {
|
|
13
|
+
|
|
14
|
+
const mockRTC = MockRTC.getRemote();
|
|
15
|
+
|
|
16
|
+
beforeEach(() => mockRTC.start());
|
|
17
|
+
afterEach(() => mockRTC.stop());
|
|
18
|
+
|
|
19
|
+
it("should be able to send a message on all data channels", async () => {
|
|
20
|
+
const mockPeer = await mockRTC.buildPeer()
|
|
21
|
+
.waitForChannel('dataChannel1')
|
|
22
|
+
.waitForChannel('dataChannel2')
|
|
23
|
+
.thenSend('Hello and goodbye');
|
|
24
|
+
|
|
25
|
+
const localConnection = new RTCPeerConnection();
|
|
26
|
+
const dataChannel1 = localConnection.createDataChannel("dataChannel1");
|
|
27
|
+
const dataChannel2 = localConnection.createDataChannel("dataChannel2");
|
|
28
|
+
|
|
29
|
+
const localOffer = await localConnection.createOffer();
|
|
30
|
+
await localConnection.setLocalDescription(localOffer);
|
|
31
|
+
const { answer } = await mockPeer.answerOffer(localOffer);
|
|
32
|
+
await localConnection.setRemoteDescription(answer);
|
|
33
|
+
|
|
34
|
+
// Wait for a response:
|
|
35
|
+
let messages: Array<any> = [];
|
|
36
|
+
dataChannel1.addEventListener('message', (event) => messages.push("1: " + event.data));
|
|
37
|
+
dataChannel2.addEventListener('message', (event) => messages.push("2: " + event.data));
|
|
38
|
+
|
|
39
|
+
await waitForChannelClose(dataChannel1);
|
|
40
|
+
|
|
41
|
+
expect(messages).to.deep.equal([
|
|
42
|
+
'1: Hello and goodbye',
|
|
43
|
+
'2: Hello and goodbye',
|
|
44
|
+
]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should be able to send a message on specific named data channels", async () => {
|
|
48
|
+
// Create a mock peer who sends 'Goodbye' after receiving its first message.
|
|
49
|
+
const mockPeer = await mockRTC.buildPeer()
|
|
50
|
+
.waitForChannel('dataChannel1')
|
|
51
|
+
.waitForChannel('dataChannel2')
|
|
52
|
+
.thenSend('dataChannel2', 'Hello and goodbye');
|
|
53
|
+
|
|
54
|
+
const localConnection = new RTCPeerConnection();
|
|
55
|
+
const dataChannel1 = localConnection.createDataChannel("dataChannel1");
|
|
56
|
+
const dataChannel2 = localConnection.createDataChannel("dataChannel2");
|
|
57
|
+
|
|
58
|
+
const localOffer = await localConnection.createOffer();
|
|
59
|
+
await localConnection.setLocalDescription(localOffer);
|
|
60
|
+
const { answer } = await mockPeer.answerOffer(localOffer);
|
|
61
|
+
await localConnection.setRemoteDescription(answer);
|
|
62
|
+
|
|
63
|
+
// Wait for a response:
|
|
64
|
+
let messages: Array<any> = [];
|
|
65
|
+
dataChannel1.addEventListener('message', (event) => messages.push("1: " + event.data));
|
|
66
|
+
dataChannel2.addEventListener('message', (event) => messages.push("2: " + event.data));
|
|
67
|
+
|
|
68
|
+
await waitForChannelClose(dataChannel1);
|
|
69
|
+
|
|
70
|
+
// We only see a 2nd channel message:
|
|
71
|
+
expect(messages).to.deep.equal([
|
|
72
|
+
'2: Hello and goodbye',
|
|
73
|
+
]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
});
|