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,53 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: 2022 Tim Perry <tim@httptoolkit.tech>
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ import { MockRTC, MockRTCOptions, MockRTCPeerBuilder } from "../mockrtc";
7
+ import { MockRTCServerPeer } from "./mockrtc-server-peer";
8
+ import { MockRTCHandlerBuilder } from "../handling/handler-builder";
9
+ import { HandlerStepDefinition } from "../handling/handler-step-definitions";
10
+ import { StepLookup } from "../handling/handler-steps";
11
+
12
+ export class MockRTCServer implements MockRTC {
13
+
14
+ constructor(
15
+ private options: MockRTCOptions = {}
16
+ ) {}
17
+
18
+ async start(): Promise<void> {}
19
+ async stop(): Promise<void> {
20
+ await Promise.all(
21
+ this.activePeers.map(peer =>
22
+ peer.close()
23
+ )
24
+ );
25
+ this._activePeers = {};
26
+ }
27
+
28
+ buildPeer(): MockRTCPeerBuilder {
29
+ return new MockRTCHandlerBuilder(this.buildPeerFromData);
30
+ }
31
+
32
+ buildPeerFromData = async (handlerStepDefinitions: HandlerStepDefinition[]): Promise<MockRTCServerPeer> => {
33
+ const handlerSteps = handlerStepDefinitions.map((definition) => {
34
+ return Object.assign(
35
+ Object.create(StepLookup[definition.type].prototype),
36
+ definition
37
+ );
38
+ });
39
+ const peer = new MockRTCServerPeer(handlerSteps, this.options);
40
+ this._activePeers[peer.peerId] = peer;
41
+ return peer;
42
+ }
43
+
44
+ private _activePeers: { [id: string]: MockRTCServerPeer } = {};
45
+ get activePeers(): Readonly<MockRTCServerPeer[]> {
46
+ return Object.values(this._activePeers);
47
+ }
48
+
49
+ getPeer(id: string): MockRTCServerPeer {
50
+ return this._activePeers[id];
51
+ }
52
+
53
+ }
@@ -0,0 +1,13 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: 2022 Tim Perry <tim@httptoolkit.tech>
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ // The WebRTC control channel name & protocol used when communicating metadata to about client
7
+ // configuration, e.g. the external connection to bridge to.
8
+ export const MOCKRTC_CONTROL_CHANNEL = "mockrtc.control-channel";
9
+
10
+ // The type of valid messages that can be sent on a control channel:
11
+ export type MockRTCControlMessage =
12
+ | { type: 'error', error: string }
13
+ | { type: 'attach-external', id: string }
@@ -0,0 +1,102 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: 2022 Tim Perry <tim@httptoolkit.tech>
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ import * as stream from 'stream';
7
+ import * as NodeDataChannel from 'node-datachannel';
8
+
9
+ /**
10
+ * Turns a node-datachannel DataChannel into a real Node.js stream, complete with
11
+ * buffering, backpressure (up to a point - if the buffer fills up, messages are dropped),
12
+ * and support for piping data elsewhere.
13
+ *
14
+ * Read & written data may be either UTF-8 strings or Buffers - this difference exists at
15
+ * the protocol level, and is preserved here throughout.
16
+ */
17
+ export class DataChannelStream extends stream.Duplex {
18
+
19
+ constructor(
20
+ private rawChannel: NodeDataChannel.DataChannel,
21
+ streamOptions: {
22
+ // These are the only Duplex options supported:
23
+ readableHighWaterMark?: number | undefined;
24
+ writableHighWaterMark?: number | undefined;
25
+ allowHalfOpen?: boolean;
26
+ } = {}
27
+ ) {
28
+ super({
29
+ allowHalfOpen: false, // Default to autoclose on end().
30
+ ...streamOptions,
31
+ objectMode: true // Preserve the string/buffer distinction (WebRTC treats them differently)
32
+ });
33
+
34
+ rawChannel.onMessage((msg) => {
35
+ if (!this._readActive) return; // If the buffer is full, drop messages.
36
+
37
+ // If the push is rejected, we pause reading until the next call to _read().
38
+ this._readActive = this.push(msg);
39
+ });
40
+
41
+ // When the DataChannel closes, the readable & writable ends close
42
+ rawChannel.onClosed(() => {
43
+ this.push(null);
44
+ this.destroy();
45
+ });
46
+
47
+ rawChannel.onError((errMsg) => {
48
+ this.destroy(new Error(`DataChannel error: ${errMsg}`));
49
+ });
50
+
51
+ // Buffer all writes until the DataChannel opens
52
+ if (!rawChannel.isOpen()) {
53
+ this.cork();
54
+ rawChannel.onOpen(() => this.uncork());
55
+ }
56
+ }
57
+
58
+ private _readActive = true;
59
+ _read() {
60
+ // Stop dropping messages, if the buffer filling up meant we were doing so before.
61
+ this._readActive = true;
62
+ }
63
+
64
+ _write(chunk: string | Buffer | unknown, encoding: string, callback: (error: Error | null) => void) {
65
+ let sentOk: boolean;
66
+
67
+ try {
68
+ if (Buffer.isBuffer(chunk)) {
69
+ sentOk = this.rawChannel.sendMessageBinary(chunk);
70
+ } else if (typeof chunk === 'string') {
71
+ sentOk = this.rawChannel.sendMessage(chunk);
72
+ } else {
73
+ const typeName = (chunk as object).constructor.name || typeof chunk;
74
+ throw new Error(`Cannot write ${typeName} to DataChannel stream`);
75
+ }
76
+ } catch (err: any) {
77
+ return callback(err);
78
+ }
79
+
80
+ if (sentOk) {
81
+ callback(null);
82
+ } else {
83
+ callback(new Error("Failed to write to DataChannel"));
84
+ }
85
+ }
86
+
87
+ _final(callback: (error: Error | null) => void) {
88
+ if (!this.allowHalfOpen) this.destroy();
89
+ callback(null);
90
+ }
91
+
92
+ _destroy(maybeErr: Error | null, callback: (error: Error | null) => void) {
93
+ // When the stream is destroyed, we close the DataChannel.
94
+ this.rawChannel.close();
95
+ callback(maybeErr);
96
+ }
97
+
98
+ get label() {
99
+ return this.rawChannel.getLabel();
100
+ }
101
+
102
+ }
@@ -0,0 +1,135 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: 2022 Tim Perry <tim@httptoolkit.tech>
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ import * as stream from 'stream';
7
+ import * as NodeDataChannel from 'node-datachannel';
8
+
9
+ const { Direction } = NodeDataChannel;
10
+
11
+ /**
12
+ * Turns a node-datachannel media track into a real Node.js stream, complete with
13
+ * buffering, backpressure (up to a point - if the buffer fills up, messages are dropped),
14
+ * and support for piping data elsewhere.
15
+ */
16
+ export class MediaTrackStream extends stream.Duplex {
17
+
18
+ constructor(
19
+ private rawTrack: NodeDataChannel.Track,
20
+ streamOptions: {
21
+ // These are the only Duplex options supported:
22
+ readableHighWaterMark?: number | undefined;
23
+ writableHighWaterMark?: number | undefined;
24
+ allowHalfOpen?: boolean;
25
+ } = {}
26
+ ) {
27
+ super({
28
+ allowHalfOpen: false, // Default to autoclose on end().
29
+ ...streamOptions,
30
+ });
31
+
32
+ rawTrack.onMessage((msg) => {
33
+ if (!this._readActive) return; // If the buffer is full, drop messages.
34
+
35
+ // If the push is rejected, we pause reading until the next call to _read().
36
+ this._readActive = this.push(msg);
37
+ });
38
+
39
+ // When the DataChannel closes, the readable & writable ends close
40
+ rawTrack.onClosed(() => this.close());
41
+
42
+ rawTrack.onError((errMsg) => {
43
+ this.destroy(new Error(`Media track error: ${errMsg}`));
44
+ });
45
+
46
+ // Buffer all writes until the DataChannel opens
47
+ if (!rawTrack.isOpen()) {
48
+ this.cork();
49
+ rawTrack.onOpen(() => this.uncork());
50
+ }
51
+ }
52
+
53
+ private close() {
54
+ this.push(null);
55
+ this.destroy();
56
+ }
57
+
58
+ private _readActive = true;
59
+ _read() {
60
+ // Stop dropping messages, if the buffer filling up meant we were doing so before.
61
+ this._readActive = true;
62
+ }
63
+
64
+ _write(chunk: Buffer, _encoding: string, callback: (error: Error | null) => void) {
65
+ let sentOk: boolean;
66
+
67
+ if (this.rawTrack.isClosed()) {
68
+ // isClosed becomes true and writes start failing just before onClosed() fires, so here we
69
+ // drop pending writes as soon as we notice.
70
+ this.close();
71
+ return;
72
+ }
73
+
74
+ try {
75
+ sentOk = this.rawTrack.sendMessageBinary(chunk);
76
+ } catch (err: any) {
77
+ return callback(err);
78
+ }
79
+
80
+ if (sentOk) {
81
+ callback(null);
82
+ } else {
83
+ callback(new Error("Failed to write to media track"));
84
+ }
85
+ }
86
+
87
+ _writev(chunks: Array<{ chunk: any; encoding: BufferEncoding; }>, callback: (error?: Error | null) => void) {
88
+ let sentOk: boolean;
89
+
90
+ if (this.rawTrack.isClosed()) {
91
+ // isClosed becomes true and writes start failing just before onClosed() fires, so here we
92
+ // drop pending writes as soon as we notice.
93
+ this.close();
94
+ return;
95
+ }
96
+
97
+ try {
98
+ sentOk = this.rawTrack.sendMessageBinary(
99
+ Buffer.concat(chunks.map(c => c.chunk))
100
+ );
101
+ } catch (err: any) {
102
+ return callback(err);
103
+ }
104
+
105
+ if (sentOk) {
106
+ callback(null);
107
+ } else {
108
+ callback(new Error("Failed to write to media track"));
109
+ }
110
+ }
111
+
112
+ _final(callback: (error: Error | null) => void) {
113
+ if (!this.allowHalfOpen) this.destroy();
114
+ callback(null);
115
+ }
116
+
117
+ _destroy(maybeErr: Error | null, callback: (error: Error | null) => void) {
118
+ // When the stream is destroyed, we close the DataChannel.
119
+ this.rawTrack.close();
120
+ callback(maybeErr);
121
+ }
122
+
123
+ get direction() {
124
+ return this.rawTrack.direction();
125
+ }
126
+
127
+ get mid() {
128
+ return this.rawTrack.mid();
129
+ }
130
+
131
+ get type() {
132
+ return this.rawTrack.type();
133
+ }
134
+
135
+ }
@@ -0,0 +1,164 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: 2022 Tim Perry <tim@httptoolkit.tech>
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ import * as NodeDataChannel from 'node-datachannel';
7
+
8
+ import { MockRTCControlMessage, MOCKRTC_CONTROL_CHANNEL } from './control-channel';
9
+
10
+ import { DataChannelStream } from './datachannel-stream';
11
+ import { MediaTrackStream } from './mediatrack-stream';
12
+ import { RTCConnection } from './rtc-connection';
13
+
14
+ export class MockRTCConnection extends RTCConnection {
15
+
16
+ // If the client supports a MockRTC control channge to send extra metadata during mocking,
17
+ // they will create this at startup, and we'll track it here, separately from all other channels.
18
+ private controlChannel: DataChannelStream | undefined;
19
+ private externalConnection: RTCConnection | undefined;
20
+
21
+ constructor(
22
+ private getExternalConnection: (id: string) => RTCConnection
23
+ ) {
24
+ super();
25
+ }
26
+
27
+ protected trackNewChannel(channel: NodeDataChannel.DataChannel, options: { isLocal: boolean }) {
28
+ if (channel.getLabel() === MOCKRTC_CONTROL_CHANNEL && !options.isLocal) {
29
+ // We don't track the control channel like other channels - we handle it specially.
30
+ if (this.controlChannel) {
31
+ const error = new Error('Cannot open multiple control channels simultaneously');
32
+ channel.sendMessage(JSON.stringify({
33
+ type: 'error',
34
+ error: error.message
35
+ }));
36
+ setTimeout(() => channel.close(), 100);
37
+ throw error;
38
+ }
39
+
40
+ this.controlChannel = new DataChannelStream(channel);
41
+
42
+ this.controlChannel.on('data', (msg) => {
43
+ try {
44
+ const controlMessage = JSON.parse(msg) as MockRTCControlMessage;
45
+
46
+ if (controlMessage.type === 'attach-external') {
47
+ if (this.externalConnection) {
48
+ throw new Error('Cannot attach mock connection to multiple external connections');
49
+ }
50
+ this.externalConnection = this.getExternalConnection(controlMessage.id);
51
+
52
+ this.emit('external-connection-attached');
53
+
54
+ // We don't necessarily proxy traffic through to the external connection at this point,
55
+ // that depends on the specific handling that's used here.
56
+ } else {
57
+ throw new Error(`Unrecognized control channel message: ${controlMessage.type}`);
58
+ }
59
+ } catch (e: any) {
60
+ console.warn("Failed to handle control channel message", e);
61
+ this.controlChannel?.write(JSON.stringify({
62
+ type: 'error',
63
+ error: e.message || e
64
+ }));
65
+ }
66
+ });
67
+
68
+ this.controlChannel.on('close', () => {
69
+ this.controlChannel = undefined;
70
+ });
71
+
72
+ this.controlChannel.on('error', (error) => {
73
+ console.error('Control channel error:', error);
74
+ });
75
+
76
+ return this.controlChannel!;
77
+ } else {
78
+ return super.trackNewChannel(channel, options);
79
+ }
80
+ }
81
+
82
+ async proxyTrafficToExternalConnection() {
83
+ if (!this.externalConnection) {
84
+ await new Promise((resolve) => this.once('external-connection-attached', resolve));
85
+ }
86
+
87
+ this.proxyTrafficTo(this.externalConnection!);
88
+ }
89
+
90
+ proxyTrafficTo(externalConnection: RTCConnection) {
91
+ /**
92
+ * When proxying traffic, you effectively have four peers, each with a connection endpoint:
93
+ * - The incoming RTCPeerConnection that we're mocking ('internal')
94
+ * - This MockRTC connection, with an associated MockRTCPeer that it will actually connect to ('mock')
95
+ * - A MockRTC external connection that will connect to the remote peer ('external')
96
+ * - The original remote peer that we're connecting to ('remote')
97
+ *
98
+ * Once the proxy is set up, the the connection structure works like so:
99
+ * INTERNAL <--> MOCK <--> EXTERNAL <--> REMOTE
100
+ *
101
+ * Here we connect the internal & external connections together, proxying all behaviours between the
102
+ * two so that from this point forwards every event on one is reflected on the other.
103
+ *
104
+ * Note that this isn't necessarily the initialization of either connection: the remote peer could
105
+ * have been connected for a while (sending data with no response), and the internal peer could have
106
+ * been fully interacting with steps before this point.
107
+ */
108
+
109
+
110
+ // Mirror connection closure:
111
+ this.on('connection-closed', () => externalConnection.close());
112
+ externalConnection.on('connection-closed', () => this.close());
113
+
114
+ /// --- Data channels: --- ///
115
+
116
+ // Forward *all* existing internal channels to the external connection:
117
+ this.channels.forEach((channel: DataChannelStream) => { // All channels, in case a previous step created one
118
+ const mirrorChannelStream = externalConnection.createDataChannel(channel.label);
119
+ channel.pipe(mirrorChannelStream).pipe(channel);
120
+ });
121
+
122
+ // Forward any existing external channels back to this peer connection. Note that we're mirroring
123
+ // *remote* channels only, so we skip the channels that we've just created above.
124
+ externalConnection.remoteChannels.forEach((channel: DataChannelStream) => {
125
+ const mirrorChannelStream = this.createDataChannel(channel.label);
126
+ channel.pipe(mirrorChannelStream).pipe(channel);
127
+ });
128
+
129
+ // If any new channels open in future, mirror them to the other peer:
130
+ [[this, externalConnection], [externalConnection, this]].forEach(([connA, connB]) => {
131
+ connA.on('remote-channel-open', (incomingChannel: DataChannelStream) => {
132
+ const mirrorChannelStream = connB.createDataChannel(incomingChannel.label);
133
+ incomingChannel.pipe(mirrorChannelStream).pipe(incomingChannel);
134
+ });
135
+ });
136
+
137
+ /// --- Media tracks: --- ///
138
+
139
+ // Note that while data channels will *not* have been negotiated before this point, so
140
+ // we can always assume that mock data channels need mirroring, media tracks are negotiated
141
+ // in the SDP, not in-band, and so any media track could already exist on the other side.
142
+
143
+ // For each track on the internal connection, proxy it to the corresponding external track:
144
+ this.mediaTracks.forEach((track: MediaTrackStream) => {
145
+ const externalStream = externalConnection.mediaTracks.find(({ mid }) => mid === track.mid);
146
+ if (externalStream) {
147
+ if (externalStream.type === track.type) {
148
+ track.pipe(externalStream).pipe(track);
149
+ } else {
150
+ throw new Error(`Mock & external streams with mid ${track.mid} have mismatched types (${
151
+ track.type
152
+ }/${
153
+ externalStream.type
154
+ })`);
155
+ }
156
+ } else {
157
+ // A mismatch in media streams means the external & mock peer negotiation isn't in sync!
158
+ // For now we just reject this case - later we should try to prompt a renegotiation.
159
+ throw new Error(`Mock has ${track.type} ${track.mid} but external does not`);
160
+ }
161
+ });
162
+ }
163
+
164
+ }