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,39 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: 2022 Tim Perry <tim@httptoolkit.tech>
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ import {
7
+ MockRTC,
8
+ waitForState,
9
+ } from '../test-setup';
10
+
11
+ describe("Close steps", function () {
12
+
13
+ this.timeout(10000); // Closing can take ~5 seconds to be recognized on the client
14
+
15
+ const mockRTC = MockRTC.getRemote();
16
+
17
+ beforeEach(() => mockRTC.start());
18
+ afterEach(() => mockRTC.stop());
19
+
20
+ it("should be able to close a connection immediately", async () => {
21
+ const mockPeer = await mockRTC.buildPeer()
22
+ .thenClose();
23
+
24
+ const localConnection = new RTCPeerConnection();
25
+
26
+ const receivedMessages: string[] = [];
27
+ const testChannel = localConnection.createDataChannel('data-channel');
28
+ testChannel.addEventListener('message', (event) => { receivedMessages.push(event.data) });
29
+
30
+ const localOffer = await localConnection.createOffer();
31
+ await localConnection.setLocalDescription(localOffer);
32
+ const { answer } = await mockPeer.answerOffer(localOffer);
33
+ await localConnection.setRemoteDescription(answer);
34
+
35
+ await waitForState(localConnection, 'connected');
36
+ await waitForState(localConnection, 'disconnected');
37
+ });
38
+
39
+ });
@@ -0,0 +1,230 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: 2022 Tim Perry <tim@httptoolkit.tech>
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ import { expect } from 'chai';
7
+ import * as SDP from 'sdp-transform';
8
+ import { MockRTC, waitForState } from '../test-setup';
9
+
10
+ describe("When connecting, MockRTC", function () {
11
+
12
+ const mockRTC = MockRTC.getRemote();
13
+
14
+ beforeEach(() => mockRTC.start());
15
+ afterEach(() => mockRTC.stop());
16
+
17
+ it("should be able create an offer and accept an answer", async () => {
18
+ const mockPeer = await mockRTC.buildPeer().waitForNextMessage().thenSend('Goodbye');
19
+
20
+ const localConnection = new RTCPeerConnection();
21
+
22
+ const { offer, setAnswer } = await mockPeer.createOffer();
23
+ await localConnection.setRemoteDescription(offer);
24
+
25
+ const localAnswer = await localConnection.createAnswer();
26
+ await localConnection.setLocalDescription(localAnswer);
27
+ await setAnswer(localAnswer);
28
+
29
+ // Wait until the connection opens successfully:
30
+ await waitForState(localConnection, 'connected');
31
+ });
32
+
33
+ it("should be able to answer a real local offer", async () => {
34
+ const mockPeer = await mockRTC.buildPeer().waitForNextMessage().thenSend('Goodbye');
35
+
36
+ const localConnection = new RTCPeerConnection();
37
+ localConnection.createDataChannel("dataChannel");
38
+
39
+ const localOffer = await localConnection.createOffer();
40
+ await localConnection.setLocalDescription(localOffer);
41
+
42
+ const { answer } = await mockPeer.answerOffer(localOffer);
43
+ await localConnection.setRemoteDescription(answer);
44
+
45
+ // Wait until the connection opens successfully:
46
+ await waitForState(localConnection, 'connected');
47
+ });
48
+
49
+ it("should be able to renegotiate after a mock offer was accepted", async () => {
50
+ const mockPeer = await mockRTC.buildPeer().waitForNextMessage().thenSend('Goodbye');
51
+
52
+ const localConnection = new RTCPeerConnection();
53
+
54
+ const { offer, setAnswer, session } = await mockPeer.createOffer();
55
+ await localConnection.setRemoteDescription(offer);
56
+
57
+ const localAnswer = await localConnection.createAnswer();
58
+ await localConnection.setLocalDescription(localAnswer);
59
+ await setAnswer(localAnswer);
60
+
61
+ // Wait until the connection opens successfully:
62
+ await waitForState(localConnection, 'connected');
63
+
64
+ // Renegotiate:
65
+ const updatedOffer = await localConnection.createOffer({ offerToReceiveAudio: true });
66
+ localConnection.setLocalDescription(updatedOffer);
67
+
68
+ const updatedAnswer = await session.answerOffer(updatedOffer);
69
+ await localConnection.setRemoteDescription(updatedAnswer);
70
+
71
+ const updatedDescription = localConnection.currentLocalDescription;
72
+ const updatedMedia = SDP.parse(updatedDescription!.sdp).media;
73
+ expect(updatedMedia.map(m => m.type)).to.include('audio');
74
+ });
75
+
76
+ it("should be able to renegotiate after answering a local offer", async () => {
77
+ const mockPeer = await mockRTC.buildPeer().waitForNextMessage().thenSend('Goodbye');
78
+
79
+ const localConnection = new RTCPeerConnection();
80
+ localConnection.createDataChannel("dataChannel");
81
+
82
+ const localOffer = await localConnection.createOffer();
83
+ await localConnection.setLocalDescription(localOffer);
84
+ const { answer, session } = await mockPeer.answerOffer(localOffer);
85
+ await localConnection.setRemoteDescription(answer);
86
+
87
+ // Wait until the connection opens successfully:
88
+ await waitForState(localConnection, 'connected');
89
+
90
+ // Renegotiate:
91
+ const updatedOffer = await localConnection.createOffer({ offerToReceiveAudio: true });
92
+ localConnection.setLocalDescription(updatedOffer);
93
+ const updatedAnswer = await session.answerOffer(updatedOffer);
94
+ await localConnection.setRemoteDescription(updatedAnswer);
95
+
96
+ const updatedDescription = localConnection.currentLocalDescription;
97
+ const updatedMedia = SDP.parse(updatedDescription!.sdp).media;
98
+ expect(updatedMedia.map(m => m.type)).to.include('audio');
99
+ });
100
+
101
+ it("should be able to create a mock offer that mirrors an existing SDP", async () => {
102
+ const mockPeer = await mockRTC.buildPeer().waitForNextMessage().thenSend('Goodbye');
103
+
104
+ const rawSdpToMirror = await (async () => {
105
+ // Wrapped in a function for clarity that this is separate, just for SDP setup:
106
+ const demoConn = new RTCPeerConnection();
107
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
108
+ const tracks = stream.getTracks();
109
+ demoConn.addTrack(tracks[0]);
110
+ return (await demoConn.createOffer({ offerToReceiveVideo: true })).sdp!;
111
+ })();
112
+ const originalSdp = SDP.parse(rawSdpToMirror);
113
+
114
+ const { offer, setAnswer } = await mockPeer.createOffer({ mirrorSDP: rawSdpToMirror });
115
+
116
+ const localConnection = new RTCPeerConnection();
117
+ await localConnection.setRemoteDescription(offer);
118
+ const localAnswer = await localConnection.createAnswer();
119
+ await localConnection.setLocalDescription(localAnswer);
120
+ await setAnswer(localAnswer);
121
+
122
+ // Wait until the connection opens successfully:
123
+ await waitForState(localConnection, 'connected');
124
+
125
+ // The remote description we accepted should match the originally mirrored SDP:
126
+ const remoteDescription = localConnection.currentRemoteDescription;
127
+ const remoteMedia = SDP.parse(remoteDescription!.sdp).media;
128
+
129
+ expect(remoteMedia.map(m => [
130
+ m.mid, m.type, m.protocol, m.direction
131
+ ])).to.deep.equal([
132
+ [0, 'audio', 'UDP/TLS/RTP/SAVPF', 'sendrecv'],
133
+ [1, 'video', 'UDP/TLS/RTP/SAVPF', 'recvonly']
134
+ ]);
135
+
136
+ // Check each individual media field. These are the fields that will be passed through when
137
+ // proxying (i.e. they're not linked to a specific peer, unlike the fingerprint etc).
138
+ ['video', 'audio'].forEach((media) => {
139
+ const originalMedia = originalSdp.media.find(({ type }) => type === media)!;
140
+ const remoteAgreedMedia = remoteMedia.find(({ type }) => type === media)!;
141
+ ([
142
+ 'msid',
143
+ 'protocol',
144
+ 'ext',
145
+ 'payloads',
146
+ 'ssrcs', // <-- Especially important, SSRC->mid is how both peers map RTP to track
147
+ 'ssrcGroups',
148
+ 'rtp',
149
+ 'fmtp',
150
+ 'rtcp',
151
+ 'rtcpFb'
152
+ ] as const).forEach((field) => {
153
+ expect(remoteAgreedMedia[field]).to.deep.equal(originalMedia[field],
154
+ `Failed to mirror ${media} ${field}`
155
+ );
156
+ });
157
+ });
158
+
159
+ // The local description is the locally-generated answer, so flips the media direction:
160
+ const localDescription = localConnection.currentLocalDescription;
161
+ const localMedia = SDP.parse(localDescription!.sdp).media;
162
+ expect(localMedia.map(m => [
163
+ m.mid, m.type, m.protocol, m.direction
164
+ ])).to.deep.equal([
165
+ [0, 'audio', 'UDP/TLS/RTP/SAVPF', 'recvonly'], // Only receive - no local audio to send
166
+ [1, 'video', 'UDP/TLS/RTP/SAVPF', 'inactive'] // Inactive - no video to send & remote is recvonly
167
+ ]);
168
+ });
169
+
170
+ it("should be able to create a mock answer that mirrors an existing SDP", async () => {
171
+ const mockPeer = await mockRTC.buildPeer().waitForNextMessage().thenSend('Goodbye');
172
+
173
+ const localConnection = new RTCPeerConnection();
174
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true });
175
+ const tracks = stream.getTracks();
176
+ localConnection.addTrack(tracks[0]);
177
+
178
+ const localOffer = await localConnection.createOffer();
179
+ await localConnection.setLocalDescription(localOffer);
180
+
181
+ const rawSdpToMirror = await (async () => {
182
+ // Wrapped in a function for clarity that this is separate, just for SDP setup:
183
+ const demoConn = new RTCPeerConnection();
184
+ demoConn.setRemoteDescription(localOffer);
185
+ demoConn.addTrack(tracks[0]);
186
+ return (await demoConn.createAnswer()).sdp!;
187
+ })();
188
+ const originalSdp = SDP.parse(rawSdpToMirror);
189
+
190
+ const { answer } = await mockPeer.answerOffer(localOffer, {
191
+ mirrorSDP: rawSdpToMirror
192
+ });
193
+ localConnection.setRemoteDescription(answer);
194
+
195
+ // Wait until the connection opens successfully:
196
+ await waitForState(localConnection, 'connected');
197
+
198
+ // The remote description we accepted should match the originally mirrored SDP:
199
+ const remoteDescription = localConnection.currentRemoteDescription;
200
+ const remoteMedia = SDP.parse(remoteDescription!.sdp).media;
201
+
202
+ expect(remoteMedia.map(m => [
203
+ m.mid, m.type, m.protocol, m.direction
204
+ ])).to.deep.equal([
205
+ [0, 'video', 'UDP/TLS/RTP/SAVPF', 'sendrecv']
206
+ ]);
207
+
208
+ // Check that the video has correct data in the fields that will be passed through when
209
+ // proxying (i.e. they're not linked to a specific peer, unlike the fingerprint etc).
210
+ const originalVideo = originalSdp.media[0];
211
+ const remoteAgreedVideo = remoteMedia[0];
212
+ ([
213
+ 'msid',
214
+ 'protocol',
215
+ 'ext',
216
+ 'payloads',
217
+ 'ssrcs', // <-- Especially important, SSRC->mid is how both peers map RTP to track
218
+ 'ssrcGroups',
219
+ 'rtp',
220
+ 'fmtp',
221
+ 'rtcp',
222
+ 'rtcpFb'
223
+ ] as const).forEach((field) => {
224
+ expect(remoteAgreedVideo[field]).to.deep.equal(originalVideo[field],
225
+ `Failed to mirror video ${field}`
226
+ );
227
+ });
228
+ });
229
+
230
+ });
@@ -0,0 +1,88 @@
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
+ delay,
10
+ waitForChannelOpen
11
+ } from '../test-setup';
12
+
13
+ describe("Echo steps", function () {
14
+
15
+ const mockRTC = MockRTC.getRemote();
16
+
17
+ beforeEach(() => mockRTC.start());
18
+ afterEach(() => mockRTC.stop());
19
+
20
+ it("should be able to echo messages across multiple data channels", async () => {
21
+ const mockPeer = await mockRTC.buildPeer()
22
+ .thenEcho();
23
+
24
+ const localConnection = new RTCPeerConnection();
25
+ const dataChannel1 = localConnection.createDataChannel("dataChannel1");
26
+
27
+ const localOffer = await localConnection.createOffer();
28
+ await localConnection.setLocalDescription(localOffer);
29
+ const { answer } = await mockPeer.answerOffer(localOffer);
30
+ await localConnection.setRemoteDescription(answer);
31
+
32
+ let messages: Array<any> = [];
33
+ dataChannel1.addEventListener('message', (event) => messages.push("1: " + event.data));
34
+
35
+ await waitForChannelOpen(dataChannel1);
36
+ dataChannel1.send('Test message 1');
37
+
38
+ const dataChannel2 = localConnection.createDataChannel("dataChannel2");
39
+ dataChannel2.addEventListener('message', (event) => messages.push("2: " + event.data));
40
+ await waitForChannelOpen(dataChannel2);
41
+ await delay(10); // Delay to guarantee ordering
42
+ dataChannel2.send('Test message 2');
43
+
44
+ await delay(10); // Delay to guarantee ordering
45
+ dataChannel1.send('Test message 3');
46
+
47
+ await delay(50); // Delay to guarantee delivery
48
+
49
+ expect(messages).to.deep.equal([
50
+ '1: Test message 1',
51
+ '2: Test message 2',
52
+ '1: Test message 3',
53
+ ]);
54
+ });
55
+
56
+ it("should be able to echo media", async () => {
57
+ const mockPeer = await mockRTC.buildPeer()
58
+ .thenEcho();
59
+
60
+ // Create a connection to send & receive video:
61
+ const localConn = new RTCPeerConnection();
62
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true });
63
+ localConn.addTrack(stream.getTracks()[0], stream);
64
+
65
+ // Turn incoming tracks into readable streams of frames:
66
+ const mediaStreamPromise = new Promise<ReadableStream<VideoFrame>>((resolve) => {
67
+ localConn.addEventListener('track', ({ track }) => {
68
+ const streamProcessor = new MediaStreamTrackProcessor({
69
+ track: track as MediaStreamVideoTrack
70
+ });
71
+ resolve(streamProcessor.readable);
72
+ });
73
+ });
74
+
75
+ // Complete the connection with the mock peer:
76
+ const localOffer = await localConn.createOffer({ offerToReceiveVideo: true });
77
+ await localConn.setLocalDescription(localOffer);
78
+ const { answer } = await mockPeer.answerOffer(localOffer);
79
+ await localConn.setRemoteDescription(answer);
80
+
81
+ // Check we receive the expected echoed video:
82
+ const localMedia = await mediaStreamPromise;
83
+ const { value: localFrame } = await localMedia!.getReader().read();
84
+ expect(localFrame!.displayHeight).to.be.greaterThanOrEqual(240);
85
+ expect(localFrame!.displayWidth).to.be.greaterThanOrEqual(320);
86
+ });
87
+
88
+ });