@dxos/network-manager 0.1.57-main.e87098f → 0.1.57
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/dist/lib/browser/{chunk-UAB4ONRL.mjs → chunk-UKMTAO4L.mjs} +553 -121
- package/dist/lib/browser/chunk-UKMTAO4L.mjs.map +7 -0
- package/dist/lib/browser/chunk-YERMWDRB.mjs +24 -0
- package/dist/lib/browser/chunk-YERMWDRB.mjs.map +7 -0
- package/dist/lib/browser/index.browser-O53CPVK3.mjs +14 -0
- package/dist/lib/browser/index.browser-O53CPVK3.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +29 -13
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +83 -38
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/node/index.cjs +1048 -124
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +1109 -151
- package/dist/lib/node/testing/index.cjs.map +4 -4
- package/dist/types/src/swarm/connection.d.ts +6 -0
- package/dist/types/src/swarm/connection.d.ts.map +1 -1
- package/dist/types/src/swarm/peer.d.ts.map +1 -1
- package/dist/types/src/testing/test-builder.d.ts +5 -2
- package/dist/types/src/testing/test-builder.d.ts.map +1 -1
- package/dist/types/src/transport/datachannel/index.browser.d.ts +15 -0
- package/dist/types/src/transport/datachannel/index.browser.d.ts.map +1 -0
- package/dist/types/src/transport/datachannel/index.d.ts +8 -0
- package/dist/types/src/transport/datachannel/index.d.ts.map +1 -0
- package/dist/types/src/transport/datachannel/rtc-data-channel.d.ts +29 -0
- package/dist/types/src/transport/datachannel/rtc-data-channel.d.ts.map +1 -0
- package/dist/types/src/transport/datachannel/rtc-events.d.ts +9 -0
- package/dist/types/src/transport/datachannel/rtc-events.d.ts.map +1 -0
- package/dist/types/src/transport/datachannel/rtc-ice-candidate.d.ts +22 -0
- package/dist/types/src/transport/datachannel/rtc-ice-candidate.d.ts.map +1 -0
- package/dist/types/src/transport/datachannel/rtc-peer-connection.d.ts +47 -0
- package/dist/types/src/transport/datachannel/rtc-peer-connection.d.ts.map +1 -0
- package/dist/types/src/transport/datachannel/rtc-peer-connection.test.d.ts +2 -0
- package/dist/types/src/transport/datachannel/rtc-peer-connection.test.d.ts.map +1 -0
- package/dist/types/src/transport/datachannel/rtc-session-description.d.ts +10 -0
- package/dist/types/src/transport/datachannel/rtc-session-description.d.ts.map +1 -0
- package/dist/types/src/transport/index.d.ts +5 -3
- package/dist/types/src/transport/index.d.ts.map +1 -1
- package/dist/types/src/transport/libdatachannel-transport.d.ts +35 -0
- package/dist/types/src/transport/libdatachannel-transport.d.ts.map +1 -0
- package/dist/types/src/transport/libdatachannel-transport.test.d.ts +2 -0
- package/dist/types/src/transport/libdatachannel-transport.test.d.ts.map +1 -0
- package/dist/types/src/transport/simplepeer-simple-peer.d.ts +2 -0
- package/dist/types/src/transport/simplepeer-simple-peer.d.ts.map +1 -0
- package/dist/types/src/transport/simplepeer-transport-proxy-test.d.ts +2 -0
- package/dist/types/src/transport/simplepeer-transport-proxy-test.d.ts.map +1 -0
- package/dist/types/src/transport/{webrtc-transport-proxy.d.ts → simplepeer-transport-proxy.d.ts} +5 -5
- package/dist/types/src/transport/simplepeer-transport-proxy.d.ts.map +1 -0
- package/dist/types/src/transport/{webrtc-transport-service.d.ts → simplepeer-transport-service.d.ts} +2 -2
- package/dist/types/src/transport/simplepeer-transport-service.d.ts.map +1 -0
- package/dist/types/src/transport/{webrtc-transport.d.ts → simplepeer-transport.d.ts} +8 -8
- package/dist/types/src/transport/simplepeer-transport.d.ts.map +1 -0
- package/dist/types/src/transport/simplepeer-transport.test.d.ts +2 -0
- package/dist/types/src/transport/simplepeer-transport.test.d.ts.map +1 -0
- package/dist/types/src/transport/transport.d.ts +6 -0
- package/dist/types/src/transport/transport.d.ts.map +1 -1
- package/dist/types/src/wire-protocol.d.ts +3 -2
- package/dist/types/src/wire-protocol.d.ts.map +1 -1
- package/package.json +25 -20
- package/src/connection-log.ts +1 -1
- package/src/swarm/connection.test.ts +8 -3
- package/src/swarm/connection.ts +58 -6
- package/src/swarm/peer.ts +4 -0
- package/src/swarm/swarm.test.ts +3 -2
- package/src/testing/test-builder.ts +72 -41
- package/src/transport/datachannel/README.md +2 -0
- package/src/transport/datachannel/index.browser.ts +8 -0
- package/src/transport/datachannel/index.ts +17 -0
- package/src/transport/datachannel/rtc-data-channel.ts +148 -0
- package/src/transport/datachannel/rtc-events.ts +23 -0
- package/src/transport/datachannel/rtc-ice-candidate.ts +54 -0
- package/src/transport/datachannel/rtc-peer-connection.test.ts +112 -0
- package/src/transport/datachannel/rtc-peer-connection.ts +347 -0
- package/src/transport/datachannel/rtc-session-description.ts +23 -0
- package/src/transport/index.ts +5 -3
- package/src/transport/libdatachannel-transport.test.ts +99 -0
- package/src/transport/libdatachannel-transport.ts +252 -0
- package/src/transport/{webrtc-transport-proxy.test.ts → simplepeer-transport-proxy-test.ts} +15 -15
- package/src/transport/{webrtc-transport-proxy.ts → simplepeer-transport-proxy.ts} +8 -8
- package/src/transport/{webrtc-transport-service.ts → simplepeer-transport-service.ts} +4 -4
- package/src/transport/{webrtc-transport.test.ts → simplepeer-transport.test.ts} +5 -5
- package/src/transport/{webrtc-transport.ts → simplepeer-transport.ts} +13 -27
- package/src/transport/transport.ts +7 -0
- package/src/wire-protocol.ts +8 -4
- package/dist/lib/browser/chunk-UAB4ONRL.mjs.map +0 -7
- package/dist/types/src/transport/webrtc-simple-peer.test.d.ts +0 -2
- package/dist/types/src/transport/webrtc-simple-peer.test.d.ts.map +0 -1
- package/dist/types/src/transport/webrtc-transport-proxy.d.ts.map +0 -1
- package/dist/types/src/transport/webrtc-transport-proxy.test.d.ts +0 -2
- package/dist/types/src/transport/webrtc-transport-proxy.test.d.ts.map +0 -1
- package/dist/types/src/transport/webrtc-transport-service.d.ts.map +0 -1
- package/dist/types/src/transport/webrtc-transport.d.ts.map +0 -1
- package/dist/types/src/transport/webrtc-transport.test.d.ts +0 -2
- package/dist/types/src/transport/webrtc-transport.test.d.ts.map +0 -1
- /package/src/transport/{webrtc-simple-peer.test.ts → simplepeer-simple-peer.ts} +0 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
// @dxos/test platform=nodejs
|
|
6
|
+
|
|
7
|
+
import { expect } from 'chai';
|
|
8
|
+
import nodeDataChannel from 'node-datachannel';
|
|
9
|
+
|
|
10
|
+
import { Trigger, asyncTimeout } from '@dxos/async';
|
|
11
|
+
import { log } from '@dxos/log';
|
|
12
|
+
import { afterTest, describe, test } from '@dxos/test';
|
|
13
|
+
|
|
14
|
+
import { PeerConnection } from './rtc-peer-connection';
|
|
15
|
+
|
|
16
|
+
describe('Node WebRTC libdatachannel implementation', () => {
|
|
17
|
+
test('Connect two datachannels', async () => {
|
|
18
|
+
const msg1to2 = 'Hello from Peer1';
|
|
19
|
+
const firstPeerReceived = new Trigger<string | Buffer>();
|
|
20
|
+
const msg2to1 = 'Hello from Peer2';
|
|
21
|
+
const secondPeerReceived = new Trigger<string | Buffer>();
|
|
22
|
+
|
|
23
|
+
let dc2: nodeDataChannel.DataChannel;
|
|
24
|
+
afterTest(() => dc2.close());
|
|
25
|
+
|
|
26
|
+
const peer1 = new nodeDataChannel.PeerConnection('Peer1', { iceServers: ['stun:stun.l.google.com:19302'] });
|
|
27
|
+
afterTest(() => peer1.close());
|
|
28
|
+
|
|
29
|
+
// Set Callbacks
|
|
30
|
+
peer1.onLocalDescription((sdp, type) => {
|
|
31
|
+
peer2.setRemoteDescription(sdp, type);
|
|
32
|
+
});
|
|
33
|
+
peer1.onLocalCandidate((candidate, mid) => {
|
|
34
|
+
peer2.addRemoteCandidate(candidate, mid);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const peer2 = new nodeDataChannel.PeerConnection('Peer2', { iceServers: ['stun:stun.l.google.com:19302'] });
|
|
38
|
+
afterTest(() => peer2.close());
|
|
39
|
+
|
|
40
|
+
// Set Callbacks
|
|
41
|
+
peer2.onLocalDescription((sdp, type) => {
|
|
42
|
+
peer1.setRemoteDescription(sdp, type);
|
|
43
|
+
});
|
|
44
|
+
peer2.onLocalCandidate((candidate, mid) => {
|
|
45
|
+
peer1.addRemoteCandidate(candidate, mid);
|
|
46
|
+
});
|
|
47
|
+
peer2.onDataChannel((dc) => {
|
|
48
|
+
dc2 = dc;
|
|
49
|
+
dc2.onMessage((msg) => secondPeerReceived.wake(msg));
|
|
50
|
+
dc2.sendMessage(msg2to1);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const dc1 = peer1.createDataChannel('test');
|
|
54
|
+
afterTest(() => dc1.close());
|
|
55
|
+
|
|
56
|
+
dc1.onOpen(() => dc1.sendMessage(msg1to2));
|
|
57
|
+
|
|
58
|
+
dc1.onMessage((msg) => firstPeerReceived.wake(msg));
|
|
59
|
+
dc1.onMessage((msg) => firstPeerReceived.wake(msg));
|
|
60
|
+
expect(await asyncTimeout(firstPeerReceived.wait(), 3000)).to.eq(msg2to1);
|
|
61
|
+
expect(await asyncTimeout(secondPeerReceived.wait(), 3000)).to.eq(msg1to2);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('RTCPeerConnection', async () => {
|
|
65
|
+
const msg1to2 = 'Hello from Peer1';
|
|
66
|
+
const firstPeerReceived = new Trigger<string | Buffer>();
|
|
67
|
+
const msg2to1 = 'Hello from Peer2';
|
|
68
|
+
const secondPeerReceived = new Trigger<string | Buffer>();
|
|
69
|
+
|
|
70
|
+
const peer1 = new PeerConnection();
|
|
71
|
+
afterTest(() => peer1.close());
|
|
72
|
+
|
|
73
|
+
const peer2 = new PeerConnection();
|
|
74
|
+
afterTest(() => peer2.close());
|
|
75
|
+
|
|
76
|
+
peer1
|
|
77
|
+
.createOffer()
|
|
78
|
+
.then(async (offer) => {
|
|
79
|
+
await peer2.setRemoteDescription(offer);
|
|
80
|
+
const answer = await peer2.createAnswer();
|
|
81
|
+
await peer1.setRemoteDescription(answer);
|
|
82
|
+
})
|
|
83
|
+
.catch((err) => {
|
|
84
|
+
log.catch(err);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
peer1.onicecandidate = async (env) => {
|
|
88
|
+
await peer2.addIceCandidate(env.candidate?.toJSON());
|
|
89
|
+
};
|
|
90
|
+
peer2.onicecandidate = async (env) => {
|
|
91
|
+
await peer1.addIceCandidate(env.candidate?.toJSON());
|
|
92
|
+
};
|
|
93
|
+
let dc2: RTCDataChannel;
|
|
94
|
+
peer2.ondatachannel = ({ channel }) => {
|
|
95
|
+
dc2 = channel;
|
|
96
|
+
dc2.onmessage = (msg) => {
|
|
97
|
+
secondPeerReceived.wake(msg.data);
|
|
98
|
+
};
|
|
99
|
+
dc2.send(msg2to1);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const dc1 = peer1.createDataChannel('test');
|
|
103
|
+
dc1.onmessage = (msg) => {
|
|
104
|
+
firstPeerReceived.wake(msg.data);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
dc1.onopen = () => dc1.send(msg1to2);
|
|
108
|
+
|
|
109
|
+
expect((await asyncTimeout(firstPeerReceived.wait(), 3000)).toString()).to.eq(msg2to1);
|
|
110
|
+
expect((await asyncTimeout(secondPeerReceived.wait(), 3000)).toString()).to.eq(msg1to2);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import node from 'node-datachannel';
|
|
6
|
+
import defer, { type DeferredPromise } from 'p-defer';
|
|
7
|
+
|
|
8
|
+
import { DataChannel } from './rtc-data-channel';
|
|
9
|
+
import { DataChannelEvent, PeerConnectionIceEvent } from './rtc-events';
|
|
10
|
+
import { IceCandidate } from './rtc-ice-candidate';
|
|
11
|
+
import { SessionDescription } from './rtc-session-description';
|
|
12
|
+
|
|
13
|
+
export class PeerConnection extends EventTarget implements RTCPeerConnection {
|
|
14
|
+
static async generateCertificate(keygenAlgorithm: AlgorithmIdentifier): Promise<RTCCertificate> {
|
|
15
|
+
throw new Error('Not implemented');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
canTrickleIceCandidates: boolean | null;
|
|
19
|
+
sctp: RTCSctpTransport | null;
|
|
20
|
+
|
|
21
|
+
onconnectionstatechange: ((this: RTCPeerConnection, ev: Event) => any) | null;
|
|
22
|
+
ondatachannel: ((this: RTCPeerConnection, ev: RTCDataChannelEvent) => any) | null;
|
|
23
|
+
onicecandidate: ((this: RTCPeerConnection, ev: RTCPeerConnectionIceEvent) => any) | null;
|
|
24
|
+
onicecandidateerror: ((this: RTCPeerConnection, ev: Event) => any) | null;
|
|
25
|
+
oniceconnectionstatechange: ((this: RTCPeerConnection, ev: Event) => any) | null;
|
|
26
|
+
onicegatheringstatechange: ((this: RTCPeerConnection, ev: Event) => any) | null;
|
|
27
|
+
onnegotiationneeded: ((this: RTCPeerConnection, ev: Event) => any) | null;
|
|
28
|
+
onsignalingstatechange: ((this: RTCPeerConnection, ev: Event) => any) | null;
|
|
29
|
+
ontrack: ((this: RTCPeerConnection, ev: RTCTrackEvent) => any) | null;
|
|
30
|
+
|
|
31
|
+
#peerConnection: node.PeerConnection;
|
|
32
|
+
#config: RTCConfiguration;
|
|
33
|
+
#localOffer: DeferredPromise<RTCSessionDescriptionInit>;
|
|
34
|
+
#localAnswer: DeferredPromise<RTCSessionDescriptionInit>;
|
|
35
|
+
#dataChannels: Set<DataChannel>;
|
|
36
|
+
|
|
37
|
+
constructor(init: RTCConfiguration = {}) {
|
|
38
|
+
super();
|
|
39
|
+
|
|
40
|
+
this.#config = init;
|
|
41
|
+
this.#localOffer = defer();
|
|
42
|
+
this.#localAnswer = defer();
|
|
43
|
+
this.#dataChannels = new Set();
|
|
44
|
+
|
|
45
|
+
const iceServers = init.iceServers ?? [];
|
|
46
|
+
|
|
47
|
+
this.#peerConnection = new node.PeerConnection(`peer-${Math.random()}`, {
|
|
48
|
+
iceServers: iceServers
|
|
49
|
+
.map((server) => {
|
|
50
|
+
const urls = (Array.isArray(server.urls) ? server.urls : [server.urls]).map((str) => new URL(str));
|
|
51
|
+
|
|
52
|
+
return urls.map((url) => {
|
|
53
|
+
/** @type {import('../lib/index.js').IceServer} */
|
|
54
|
+
const iceServer = {
|
|
55
|
+
hostname: url.hostname,
|
|
56
|
+
port: parseInt(url.port, 10),
|
|
57
|
+
username: server.username,
|
|
58
|
+
password: server.credential,
|
|
59
|
+
// relayType - how to specify?
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return iceServer;
|
|
63
|
+
});
|
|
64
|
+
})
|
|
65
|
+
.flat(),
|
|
66
|
+
iceTransportPolicy: init?.iceTransportPolicy,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
this.#peerConnection.onStateChange(() => {
|
|
70
|
+
this.dispatchEvent(new Event('connectionstatechange'));
|
|
71
|
+
});
|
|
72
|
+
// https://github.com/murat-dogan/node-datachannel/pull/171
|
|
73
|
+
// this.#peerConnection.onSignalingStateChange(() => {
|
|
74
|
+
// this.dispatchEvent(new Event('signalingstatechange'))
|
|
75
|
+
// })
|
|
76
|
+
this.#peerConnection.onGatheringStateChange(() => {
|
|
77
|
+
this.dispatchEvent(new Event('icegatheringstatechange'));
|
|
78
|
+
});
|
|
79
|
+
this.#peerConnection.onDataChannel((channel) => {
|
|
80
|
+
this.dispatchEvent(new DataChannelEvent(new DataChannel(channel)));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// forward events to properties
|
|
84
|
+
this.addEventListener('connectionstatechange', (event) => {
|
|
85
|
+
this.onconnectionstatechange?.(event);
|
|
86
|
+
});
|
|
87
|
+
this.addEventListener('signalingstatechange', (event) => {
|
|
88
|
+
this.onsignalingstatechange?.(event);
|
|
89
|
+
});
|
|
90
|
+
this.addEventListener('icegatheringstatechange', (event) => {
|
|
91
|
+
this.onicegatheringstatechange?.(event);
|
|
92
|
+
});
|
|
93
|
+
this.addEventListener('datachannel', (event) => {
|
|
94
|
+
this.ondatachannel?.(event as RTCDataChannelEvent);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
this.#peerConnection.onLocalDescription((sdp, type) => {
|
|
98
|
+
if (type === 'offer') {
|
|
99
|
+
this.#localOffer.resolve({
|
|
100
|
+
sdp,
|
|
101
|
+
type,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (type === 'answer') {
|
|
106
|
+
this.#localAnswer.resolve({
|
|
107
|
+
sdp,
|
|
108
|
+
type,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
this.#peerConnection.onLocalCandidate((candidate, mid) => {
|
|
114
|
+
if (mid === 'unspec') {
|
|
115
|
+
this.#localAnswer.reject(new Error(`Invalid description type ${mid}`));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const event = new PeerConnectionIceEvent(new IceCandidate({ candidate }));
|
|
120
|
+
|
|
121
|
+
this.onicecandidate?.(event);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
this.canTrickleIceCandidates = null;
|
|
125
|
+
this.sctp = null;
|
|
126
|
+
this.onconnectionstatechange = null;
|
|
127
|
+
this.ondatachannel = null;
|
|
128
|
+
this.onicecandidate = null;
|
|
129
|
+
this.onicecandidateerror = null;
|
|
130
|
+
this.oniceconnectionstatechange = null;
|
|
131
|
+
this.onicegatheringstatechange = null;
|
|
132
|
+
this.onnegotiationneeded = null;
|
|
133
|
+
this.onsignalingstatechange = null;
|
|
134
|
+
this.ontrack = null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
get connectionState(): RTCPeerConnectionState {
|
|
138
|
+
return assertState(this.#peerConnection.state(), RTCPeerConnectionStates);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
get iceConnectionState(): RTCIceConnectionState {
|
|
142
|
+
return assertState(this.#peerConnection.state(), RTCIceConnectionStates);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
get iceGatheringState(): RTCIceGatheringState {
|
|
146
|
+
return assertState(this.#peerConnection.gatheringState(), RTCIceGatheringStates);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
get signalingState(): RTCSignalingState {
|
|
150
|
+
return assertState(this.#peerConnection.signalingState(), RTCSignalingStates);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
get currentLocalDescription(): RTCSessionDescription | null {
|
|
154
|
+
return toSessionDescription(this.#peerConnection.localDescription());
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
get localDescription(): RTCSessionDescription | null {
|
|
158
|
+
return toSessionDescription(this.#peerConnection.localDescription());
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
get pendingLocalDescription(): RTCSessionDescription | null {
|
|
162
|
+
return toSessionDescription(this.#peerConnection.localDescription());
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
get currentRemoteDescription(): RTCSessionDescription | null {
|
|
166
|
+
// not exposed by node-datachannel
|
|
167
|
+
console.log("node-datachannel doesn't expose currentRemoteDescription");
|
|
168
|
+
return toSessionDescription(null);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
get pendingRemoteDescription(): RTCSessionDescription | null {
|
|
172
|
+
// not exposed by node-datachannel
|
|
173
|
+
console.log("node-datachannel doesn't expose pendingRemoteDescription");
|
|
174
|
+
return toSessionDescription(null);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
get remoteDescription(): RTCSessionDescription | null {
|
|
178
|
+
// not exposed by node-datachannel
|
|
179
|
+
console.log("node-datachannel doesn't expose remoteDescription");
|
|
180
|
+
return toSessionDescription(null);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async addIceCandidate(candidate?: RTCIceCandidateInit): Promise<void> {
|
|
184
|
+
if (candidate == null || candidate.candidate == null) {
|
|
185
|
+
throw new Error('Candidate invalid');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this.#peerConnection.addRemoteCandidate(candidate.candidate, candidate.sdpMid ?? '0');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
addTrack(track: MediaStreamTrack, ...streams: MediaStream[]): RTCRtpSender {
|
|
192
|
+
throw new Error('addTrack Not implemented');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
addTransceiver(trackOrKind: MediaStreamTrack | string, init?: RTCRtpTransceiverInit): RTCRtpTransceiver {
|
|
196
|
+
throw new Error('addTransciever Not implemented');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
close(): void {
|
|
200
|
+
// close all channels before shutting down
|
|
201
|
+
this.#dataChannels.forEach((channel) => {
|
|
202
|
+
channel.close();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
this.#peerConnection.close();
|
|
206
|
+
this.#peerConnection.destroy();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
createDataChannel(label: string, dataChannelDict: RTCDataChannelInit = {}): RTCDataChannel {
|
|
210
|
+
const channel = this.#peerConnection.createDataChannel(label, dataChannelDict);
|
|
211
|
+
const dataChannel = new DataChannel(channel, dataChannelDict);
|
|
212
|
+
|
|
213
|
+
// ensure we can close all channels when shutting down
|
|
214
|
+
this.#dataChannels.add(dataChannel);
|
|
215
|
+
dataChannel.addEventListener('close', () => {
|
|
216
|
+
this.#dataChannels.delete(dataChannel);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
return dataChannel;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async createOffer(options?: RTCOfferOptions): Promise<RTCSessionDescriptionInit>;
|
|
223
|
+
async createOffer(
|
|
224
|
+
successCallback: RTCSessionDescriptionCallback,
|
|
225
|
+
failureCallback: RTCPeerConnectionErrorCallback,
|
|
226
|
+
options?: RTCOfferOptions,
|
|
227
|
+
): Promise<void>;
|
|
228
|
+
|
|
229
|
+
async createOffer(...args: any[]): Promise<any> {
|
|
230
|
+
return this.#localOffer.promise;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async createAnswer(options?: RTCAnswerOptions): Promise<RTCSessionDescriptionInit>;
|
|
234
|
+
async createAnswer(
|
|
235
|
+
successCallback: RTCSessionDescriptionCallback,
|
|
236
|
+
failureCallback: RTCPeerConnectionErrorCallback,
|
|
237
|
+
): Promise<void>;
|
|
238
|
+
|
|
239
|
+
async createAnswer(...args: any[]): Promise<any> {
|
|
240
|
+
return this.#localAnswer.promise;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
getConfiguration(): RTCConfiguration {
|
|
244
|
+
return this.#config;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
getReceivers(): RTCRtpReceiver[] {
|
|
248
|
+
throw new Error('getReceivers Not implemented');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
getSenders(): RTCRtpSender[] {
|
|
252
|
+
throw new Error('getSenders Not implemented');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async getStats(selector?: MediaStreamTrack | null): Promise<RTCStatsReport> {
|
|
256
|
+
throw new Error('getStats Not implemented');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
getTransceivers(): RTCRtpTransceiver[] {
|
|
260
|
+
throw new Error('getTranscievers Not implemented');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
removeTrack(sender: RTCRtpSender): void {
|
|
264
|
+
throw new Error('removeTrack Not implemented');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
restartIce(): void {
|
|
268
|
+
throw new Error('restartIce Not implemented');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
setConfiguration(configuration: RTCConfiguration = {}): void {
|
|
272
|
+
this.#config = configuration;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async setLocalDescription(description?: RTCLocalSessionDescriptionInit): Promise<void> {
|
|
276
|
+
if (description == null || description.type == null) {
|
|
277
|
+
throw new Error('Local description type must be set');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (description.type !== 'offer') {
|
|
281
|
+
console.log(`node-datachannel: setLocalDescription: only offer is supported, not ${description.type}`);
|
|
282
|
+
// any other type causes libdatachannel to throw
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// @ts-expect-error types are wrong
|
|
287
|
+
this.#peerConnection.setLocalDescription(description.type);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async setRemoteDescription(description: RTCSessionDescriptionInit): Promise<void> {
|
|
291
|
+
if (description.sdp == null) {
|
|
292
|
+
throw new Error('Remote SDP must be set');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// @ts-expect-error types are wrong
|
|
296
|
+
this.#peerConnection.setRemoteDescription(description.sdp, description.type);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export { PeerConnection as RTCPeerConnection };
|
|
301
|
+
|
|
302
|
+
const assertState = <T>(state: any, states: T[]): T => {
|
|
303
|
+
if (state != null && !states.includes(state)) {
|
|
304
|
+
throw new Error(`Invalid value encountered - "${state}" must be one of ${states}`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return state as T;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const toSessionDescription = (description: { sdp?: string; type: string } | null): RTCSessionDescription | null => {
|
|
311
|
+
if (description == null) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return new SessionDescription({
|
|
316
|
+
sdp: description.sdp,
|
|
317
|
+
type: assertState(description.type, RTCSdpTypes),
|
|
318
|
+
});
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const RTCPeerConnectionStates: RTCPeerConnectionState[] = [
|
|
322
|
+
'closed',
|
|
323
|
+
'connected',
|
|
324
|
+
'connecting',
|
|
325
|
+
'disconnected',
|
|
326
|
+
'failed',
|
|
327
|
+
'new',
|
|
328
|
+
];
|
|
329
|
+
const RTCSdpTypes: RTCSdpType[] = ['answer', 'offer', 'pranswer', 'rollback'];
|
|
330
|
+
const RTCIceConnectionStates: RTCIceConnectionState[] = [
|
|
331
|
+
'checking',
|
|
332
|
+
'closed',
|
|
333
|
+
'completed',
|
|
334
|
+
'connected',
|
|
335
|
+
'disconnected',
|
|
336
|
+
'failed',
|
|
337
|
+
'new',
|
|
338
|
+
];
|
|
339
|
+
const RTCIceGatheringStates: RTCIceGatheringState[] = ['complete', 'gathering', 'new'];
|
|
340
|
+
const RTCSignalingStates: RTCSignalingState[] = [
|
|
341
|
+
'closed',
|
|
342
|
+
'have-local-offer',
|
|
343
|
+
'have-local-pranswer',
|
|
344
|
+
'have-remote-offer',
|
|
345
|
+
'have-remote-pranswer',
|
|
346
|
+
'stable',
|
|
347
|
+
];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @see https://developer.mozilla.org/docs/Web/API/RTCSessionDescription
|
|
7
|
+
*/
|
|
8
|
+
export class SessionDescription implements RTCSessionDescription {
|
|
9
|
+
readonly sdp: string;
|
|
10
|
+
readonly type: RTCSdpType;
|
|
11
|
+
|
|
12
|
+
constructor(init: RTCSessionDescriptionInit) {
|
|
13
|
+
this.sdp = init.sdp ?? '';
|
|
14
|
+
this.type = init.type;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
toJSON(): RTCSessionDescriptionInit {
|
|
18
|
+
return {
|
|
19
|
+
sdp: this.sdp,
|
|
20
|
+
type: this.type,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/transport/index.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
export * from './memory-transport';
|
|
6
6
|
export * from './transport';
|
|
7
|
-
export * from './
|
|
8
|
-
export * from './
|
|
9
|
-
export * from './
|
|
7
|
+
export * from './simplepeer-transport';
|
|
8
|
+
export * from './simplepeer-transport-service';
|
|
9
|
+
export * from './simplepeer-transport-proxy';
|
|
10
|
+
export * from './datachannel';
|
|
11
|
+
export * from './libdatachannel-transport';
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
//
|
|
6
|
+
// Copyright 2020 DXOS.org
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
import { Duplex } from 'stream';
|
|
10
|
+
|
|
11
|
+
import { sleep, TestStream } from '@dxos/async';
|
|
12
|
+
import { log } from '@dxos/log';
|
|
13
|
+
import { afterTest, describe, test } from '@dxos/test';
|
|
14
|
+
|
|
15
|
+
import { LibDataChannelTransport } from './libdatachannel-transport';
|
|
16
|
+
import { SimplePeerTransport } from './simplepeer-transport';
|
|
17
|
+
|
|
18
|
+
describe('LibDataChannelTransport', () => {
|
|
19
|
+
test('open and close', async () => {
|
|
20
|
+
const connection = new LibDataChannelTransport({
|
|
21
|
+
initiator: true,
|
|
22
|
+
stream: new Duplex(),
|
|
23
|
+
sendSignal: async () => {},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const wait = connection.closed.waitForCount(1);
|
|
27
|
+
await connection.destroy();
|
|
28
|
+
await wait;
|
|
29
|
+
})
|
|
30
|
+
.onlyEnvironments('nodejs')
|
|
31
|
+
.timeout(1_000)
|
|
32
|
+
.retries(3);
|
|
33
|
+
|
|
34
|
+
test('establish connection and send data through with protocol', async () => {
|
|
35
|
+
const stream1 = new TestStream();
|
|
36
|
+
const connection1 = new LibDataChannelTransport({
|
|
37
|
+
initiator: true,
|
|
38
|
+
stream: stream1,
|
|
39
|
+
sendSignal: async (signal) => {
|
|
40
|
+
await sleep(10);
|
|
41
|
+
await connection2.signal(signal);
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
afterTest(() => connection1.destroy());
|
|
45
|
+
afterTest(() => connection1.errors.assertNoUnhandledErrors());
|
|
46
|
+
|
|
47
|
+
const stream2 = new TestStream();
|
|
48
|
+
const connection2 = new LibDataChannelTransport({
|
|
49
|
+
initiator: false,
|
|
50
|
+
stream: stream2,
|
|
51
|
+
sendSignal: async (signal) => {
|
|
52
|
+
await sleep(10);
|
|
53
|
+
await connection1.signal(signal);
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
afterTest(() => connection2.destroy());
|
|
57
|
+
afterTest(() => connection2.errors.assertNoUnhandledErrors());
|
|
58
|
+
|
|
59
|
+
await TestStream.assertConnectivity(stream1, stream2, { timeout: 2_000 });
|
|
60
|
+
})
|
|
61
|
+
.onlyEnvironments('nodejs')
|
|
62
|
+
.timeout(2_000)
|
|
63
|
+
.retries(3);
|
|
64
|
+
|
|
65
|
+
test('establish connection between LibDataChannel and SimplePeer', async () => {
|
|
66
|
+
const stream1 = new TestStream();
|
|
67
|
+
const connection1 = new LibDataChannelTransport({
|
|
68
|
+
initiator: true,
|
|
69
|
+
stream: stream1,
|
|
70
|
+
sendSignal: async (signal) => {
|
|
71
|
+
log.debug('signal', signal);
|
|
72
|
+
|
|
73
|
+
await sleep(10);
|
|
74
|
+
await connection2.signal(signal);
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
afterTest(() => connection1.destroy());
|
|
78
|
+
afterTest(() => connection1.errors.assertNoUnhandledErrors());
|
|
79
|
+
|
|
80
|
+
const stream2 = new TestStream();
|
|
81
|
+
const connection2 = new SimplePeerTransport({
|
|
82
|
+
initiator: false,
|
|
83
|
+
stream: stream2,
|
|
84
|
+
sendSignal: async (signal) => {
|
|
85
|
+
log.debug('signal', signal);
|
|
86
|
+
|
|
87
|
+
await sleep(10);
|
|
88
|
+
await connection1.signal(signal);
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
afterTest(() => connection2.destroy());
|
|
92
|
+
afterTest(() => connection2.errors.assertNoUnhandledErrors());
|
|
93
|
+
|
|
94
|
+
await TestStream.assertConnectivity(stream1, stream2, { timeout: 2_000 });
|
|
95
|
+
})
|
|
96
|
+
.onlyEnvironments('nodejs')
|
|
97
|
+
.timeout(2_000)
|
|
98
|
+
.retries(3);
|
|
99
|
+
});
|