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.
Files changed (136) hide show
  1. package/.github/workflows/ci.yml +29 -0
  2. package/LICENSE +201 -0
  3. package/README.md +290 -0
  4. package/dist/admin-bin.d.ts +2 -0
  5. package/dist/admin-bin.js +67 -0
  6. package/dist/admin-bin.js.map +1 -0
  7. package/dist/client/mockrtc-client.d.ts +12 -0
  8. package/dist/client/mockrtc-client.js +67 -0
  9. package/dist/client/mockrtc-client.js.map +1 -0
  10. package/dist/client/mockrtc-remote-peer.d.ts +15 -0
  11. package/dist/client/mockrtc-remote-peer.js +246 -0
  12. package/dist/client/mockrtc-remote-peer.js.map +1 -0
  13. package/dist/control-channel.d.ts +8 -0
  14. package/dist/control-channel.js +11 -0
  15. package/dist/control-channel.js.map +1 -0
  16. package/dist/handling/handler-builder.d.ts +138 -0
  17. package/dist/handling/handler-builder.js +164 -0
  18. package/dist/handling/handler-builder.js.map +1 -0
  19. package/dist/handling/handler-step-definitions.d.ts +63 -0
  20. package/dist/handling/handler-step-definitions.js +123 -0
  21. package/dist/handling/handler-step-definitions.js.map +1 -0
  22. package/dist/handling/handler-steps.d.ts +48 -0
  23. package/dist/handling/handler-steps.js +218 -0
  24. package/dist/handling/handler-steps.js.map +1 -0
  25. package/dist/main-browser.d.ts +9 -0
  26. package/dist/main-browser.js +26 -0
  27. package/dist/main-browser.js.map +1 -0
  28. package/dist/main.d.ts +58 -0
  29. package/dist/main.js +67 -0
  30. package/dist/main.js.map +1 -0
  31. package/dist/mockrtc-admin-plugin.d.ts +56 -0
  32. package/dist/mockrtc-admin-plugin.js +151 -0
  33. package/dist/mockrtc-admin-plugin.js.map +1 -0
  34. package/dist/mockrtc-admin-server.d.ts +7 -0
  35. package/dist/mockrtc-admin-server.js +18 -0
  36. package/dist/mockrtc-admin-server.js.map +1 -0
  37. package/dist/mockrtc-client.d.ts +12 -0
  38. package/dist/mockrtc-client.js +64 -0
  39. package/dist/mockrtc-client.js.map +1 -0
  40. package/dist/mockrtc-handler-builder.d.ts +15 -0
  41. package/dist/mockrtc-handler-builder.js +24 -0
  42. package/dist/mockrtc-handler-builder.js.map +1 -0
  43. package/dist/mockrtc-peer.d.ts +147 -0
  44. package/dist/mockrtc-peer.js +7 -0
  45. package/dist/mockrtc-peer.js.map +1 -0
  46. package/dist/mockrtc-remote-peer.d.ts +15 -0
  47. package/dist/mockrtc-remote-peer.js +234 -0
  48. package/dist/mockrtc-remote-peer.js.map +1 -0
  49. package/dist/mockrtc-server-peer.d.ts +29 -0
  50. package/dist/mockrtc-server-peer.js +145 -0
  51. package/dist/mockrtc-server-peer.js.map +1 -0
  52. package/dist/mockrtc-server.d.ts +14 -0
  53. package/dist/mockrtc-server.js +53 -0
  54. package/dist/mockrtc-server.js.map +1 -0
  55. package/dist/mockrtc.d.ts +25 -0
  56. package/dist/mockrtc.js +7 -0
  57. package/dist/mockrtc.js.map +1 -0
  58. package/dist/package.json +52 -0
  59. package/dist/server/mockrtc-admin-plugin.d.ts +17 -0
  60. package/dist/server/mockrtc-admin-plugin.js +163 -0
  61. package/dist/server/mockrtc-admin-plugin.js.map +1 -0
  62. package/dist/server/mockrtc-admin-server.d.ts +7 -0
  63. package/dist/server/mockrtc-admin-server.js +18 -0
  64. package/dist/server/mockrtc-admin-server.js.map +1 -0
  65. package/dist/server/mockrtc-server-peer.d.ts +24 -0
  66. package/dist/server/mockrtc-server-peer.js +141 -0
  67. package/dist/server/mockrtc-server-peer.js.map +1 -0
  68. package/dist/server/mockrtc-server.d.ts +14 -0
  69. package/dist/server/mockrtc-server.js +53 -0
  70. package/dist/server/mockrtc-server.js.map +1 -0
  71. package/dist/src/main.d.ts +1 -0
  72. package/dist/src/main.js +24 -0
  73. package/dist/src/main.js.map +1 -0
  74. package/dist/src/mockrtc-peer.d.ts +0 -0
  75. package/dist/src/mockrtc-peer.js +2 -0
  76. package/dist/src/mockrtc-peer.js.map +1 -0
  77. package/dist/src/mockrtc.d.ts +0 -0
  78. package/dist/src/mockrtc.js +65 -0
  79. package/dist/src/mockrtc.js.map +1 -0
  80. package/dist/webrtc/control-channel.d.ts +8 -0
  81. package/dist/webrtc/control-channel.js +11 -0
  82. package/dist/webrtc/control-channel.js.map +1 -0
  83. package/dist/webrtc/datachannel-stream.d.ts +25 -0
  84. package/dist/webrtc/datachannel-stream.js +86 -0
  85. package/dist/webrtc/datachannel-stream.js.map +1 -0
  86. package/dist/webrtc/mediatrack-stream.d.ts +29 -0
  87. package/dist/webrtc/mediatrack-stream.js +109 -0
  88. package/dist/webrtc/mediatrack-stream.js.map +1 -0
  89. package/dist/webrtc/mockrtc-connection.d.ts +14 -0
  90. package/dist/webrtc/mockrtc-connection.js +147 -0
  91. package/dist/webrtc/mockrtc-connection.js.map +1 -0
  92. package/dist/webrtc/peer-connection.d.ts +16 -0
  93. package/dist/webrtc/peer-connection.js +81 -0
  94. package/dist/webrtc/peer-connection.js.map +1 -0
  95. package/dist/webrtc/rtc-connection.d.ts +47 -0
  96. package/dist/webrtc/rtc-connection.js +370 -0
  97. package/dist/webrtc/rtc-connection.js.map +1 -0
  98. package/dist/webrtc-hooks.d.ts +30 -0
  99. package/dist/webrtc-hooks.js +224 -0
  100. package/dist/webrtc-hooks.js.map +1 -0
  101. package/karma.conf.ts +89 -0
  102. package/ngi-eu-footer.png +0 -0
  103. package/package.json +86 -0
  104. package/src/admin-bin.ts +57 -0
  105. package/src/client/mockrtc-client.ts +79 -0
  106. package/src/client/mockrtc-remote-peer.ts +286 -0
  107. package/src/handling/handler-builder.ts +215 -0
  108. package/src/handling/handler-step-definitions.ts +142 -0
  109. package/src/handling/handler-steps.ts +254 -0
  110. package/src/main-browser.ts +44 -0
  111. package/src/main.ts +109 -0
  112. package/src/mockrtc-peer.ts +176 -0
  113. package/src/mockrtc.ts +36 -0
  114. package/src/server/mockrtc-admin-plugin.ts +196 -0
  115. package/src/server/mockrtc-admin-server.ts +17 -0
  116. package/src/server/mockrtc-server-peer.ts +159 -0
  117. package/src/server/mockrtc-server.ts +53 -0
  118. package/src/webrtc/control-channel.ts +13 -0
  119. package/src/webrtc/datachannel-stream.ts +102 -0
  120. package/src/webrtc/mediatrack-stream.ts +135 -0
  121. package/src/webrtc/mockrtc-connection.ts +164 -0
  122. package/src/webrtc/rtc-connection.ts +420 -0
  123. package/src/webrtc-hooks.ts +245 -0
  124. package/test/integration/close-steps.spec.ts +39 -0
  125. package/test/integration/connection-setup.spec.ts +230 -0
  126. package/test/integration/echo-steps.spec.ts +88 -0
  127. package/test/integration/proxy.spec.ts +526 -0
  128. package/test/integration/send-steps.spec.ts +76 -0
  129. package/test/integration/smoke-test.spec.ts +100 -0
  130. package/test/integration/wait-steps.spec.ts +225 -0
  131. package/test/start-test-admin-server.ts +12 -0
  132. package/test/test-setup.ts +136 -0
  133. package/test/tsconfig.json +11 -0
  134. package/tsconfig.json +14 -0
  135. package/typedoc.json +19 -0
  136. 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
+ });